@fluojs/http 1.0.0-beta.1 → 1.0.0-beta.11

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 (106) hide show
  1. package/README.ko.md +39 -5
  2. package/README.md +39 -5
  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 +54 -55
  9. package/dist/adapters/dto-binding-plan.d.ts +22 -0
  10. package/dist/adapters/dto-binding-plan.d.ts.map +1 -0
  11. package/dist/adapters/dto-binding-plan.js +86 -0
  12. package/dist/adapters/dto-validation-adapter.d.ts +3 -1
  13. package/dist/adapters/dto-validation-adapter.d.ts.map +1 -1
  14. package/dist/adapters/dto-validation-adapter.js +10 -16
  15. package/dist/context/request-context-node-store.d.ts +25 -0
  16. package/dist/context/request-context-node-store.d.ts.map +1 -0
  17. package/dist/context/request-context-node-store.js +32 -0
  18. package/dist/context/request-context-stack-store.d.ts +8 -0
  19. package/dist/context/request-context-stack-store.d.ts.map +1 -0
  20. package/dist/context/request-context-stack-store.js +29 -0
  21. package/dist/context/request-context-store.d.ts +7 -0
  22. package/dist/context/request-context-store.d.ts.map +1 -0
  23. package/dist/context/request-context-store.js +1 -0
  24. package/dist/context/request-context.d.ts +7 -3
  25. package/dist/context/request-context.d.ts.map +1 -1
  26. package/dist/context/request-context.js +17 -5
  27. package/dist/context/sse.d.ts +38 -0
  28. package/dist/context/sse.d.ts.map +1 -1
  29. package/dist/context/sse.js +50 -2
  30. package/dist/decorators.d.ts.map +1 -1
  31. package/dist/decorators.js +262 -53
  32. package/dist/dispatch/dispatch-content-negotiation.d.ts +17 -0
  33. package/dist/dispatch/dispatch-content-negotiation.d.ts.map +1 -1
  34. package/dist/dispatch/dispatch-content-negotiation.js +21 -0
  35. package/dist/dispatch/dispatch-error-policy.d.ts +8 -0
  36. package/dist/dispatch/dispatch-error-policy.d.ts.map +1 -1
  37. package/dist/dispatch/dispatch-error-policy.js +9 -0
  38. package/dist/dispatch/dispatch-handler-policy.d.ts +11 -1
  39. package/dist/dispatch/dispatch-handler-policy.d.ts.map +1 -1
  40. package/dist/dispatch/dispatch-handler-policy.js +17 -5
  41. package/dist/dispatch/dispatch-response-policy.d.ts +11 -1
  42. package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -1
  43. package/dist/dispatch/dispatch-response-policy.js +44 -2
  44. package/dist/dispatch/dispatch-routing-policy.d.ts +13 -0
  45. package/dist/dispatch/dispatch-routing-policy.d.ts.map +1 -1
  46. package/dist/dispatch/dispatch-routing-policy.js +49 -4
  47. package/dist/dispatch/dispatcher.d.ts +24 -7
  48. package/dist/dispatch/dispatcher.d.ts.map +1 -1
  49. package/dist/dispatch/dispatcher.js +464 -48
  50. package/dist/dispatch/fast-path/debug-visibility.d.ts +18 -0
  51. package/dist/dispatch/fast-path/debug-visibility.d.ts.map +1 -0
  52. package/dist/dispatch/fast-path/debug-visibility.js +39 -0
  53. package/dist/dispatch/fast-path/eligibility-checker.d.ts +22 -0
  54. package/dist/dispatch/fast-path/eligibility-checker.d.ts.map +1 -0
  55. package/dist/dispatch/fast-path/eligibility-checker.js +107 -0
  56. package/dist/dispatch/fast-path/eligibility.d.ts +61 -0
  57. package/dist/dispatch/fast-path/eligibility.d.ts.map +1 -0
  58. package/dist/dispatch/fast-path/eligibility.js +23 -0
  59. package/dist/dispatch/fast-path/fast-path-executor.d.ts +21 -0
  60. package/dist/dispatch/fast-path/fast-path-executor.d.ts.map +1 -0
  61. package/dist/dispatch/fast-path/fast-path-executor.js +80 -0
  62. package/dist/dispatch/fast-path/index.d.ts +6 -0
  63. package/dist/dispatch/fast-path/index.d.ts.map +1 -0
  64. package/dist/dispatch/fast-path/index.js +4 -0
  65. package/dist/dispatch/native-route-handoff.d.ts +53 -0
  66. package/dist/dispatch/native-route-handoff.d.ts.map +1 -0
  67. package/dist/dispatch/native-route-handoff.js +97 -0
  68. package/dist/errors.d.ts +3 -0
  69. package/dist/errors.d.ts.map +1 -1
  70. package/dist/errors.js +4 -0
  71. package/dist/guards.d.ts +7 -0
  72. package/dist/guards.d.ts.map +1 -1
  73. package/dist/guards.js +11 -0
  74. package/dist/index.d.ts +2 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +1 -0
  77. package/dist/input-error-detail.d.ts +9 -0
  78. package/dist/input-error-detail.d.ts.map +1 -1
  79. package/dist/input-error-detail.js +10 -0
  80. package/dist/interceptors.d.ts +8 -0
  81. package/dist/interceptors.d.ts.map +1 -1
  82. package/dist/interceptors.js +14 -1
  83. package/dist/internal.d.ts +3 -0
  84. package/dist/internal.d.ts.map +1 -1
  85. package/dist/internal.js +3 -1
  86. package/dist/mapping.d.ts +7 -0
  87. package/dist/mapping.d.ts.map +1 -1
  88. package/dist/mapping.js +93 -11
  89. package/dist/middleware/correlation.d.ts +5 -0
  90. package/dist/middleware/correlation.d.ts.map +1 -1
  91. package/dist/middleware/correlation.js +14 -2
  92. package/dist/middleware/cors.d.ts +9 -0
  93. package/dist/middleware/cors.d.ts.map +1 -1
  94. package/dist/middleware/cors.js +11 -0
  95. package/dist/middleware/middleware.d.ts +34 -0
  96. package/dist/middleware/middleware.d.ts.map +1 -1
  97. package/dist/middleware/middleware.js +47 -0
  98. package/dist/middleware/security-headers.d.ts +9 -0
  99. package/dist/middleware/security-headers.d.ts.map +1 -1
  100. package/dist/middleware/security-headers.js +11 -0
  101. package/dist/route-path.d.ts +41 -0
  102. package/dist/route-path.d.ts.map +1 -1
  103. package/dist/route-path.js +50 -0
  104. package/dist/types.d.ts +17 -0
  105. package/dist/types.d.ts.map +1 -1
  106. package/package.json +4 -4
@@ -1,21 +1,23 @@
1
- import { invokeControllerHandler } from './dispatch-handler-policy.js';
2
- import { resolveContentNegotiation, writeErrorResponse, writeSuccessResponse } from './dispatch-response-policy.js';
3
- import { matchHandlerOrThrow, updateRequestParams } from './dispatch-routing-policy.js';
1
+ import { getCompiledDtoBindingPlan } from '../adapters/dto-binding-plan.js';
2
+ import { createRequestContext, runWithRequestContext } from '../context/request-context.js';
3
+ import { SseResponse } from '../context/sse.js';
4
4
  import { RequestAbortedError } from '../errors.js';
5
5
  import { runGuardChain } from '../guards.js';
6
6
  import { runInterceptorChain } from '../interceptors.js';
7
- import { runMiddlewareChain } from '../middleware/middleware.js';
8
- import { createRequestContext, runWithRequestContext } from '../context/request-context.js';
9
- import { SseResponse } from '../context/sse.js';
7
+ import { isMiddlewareRouteConfig, matchRoutePattern, runMiddlewareChain } from '../middleware/middleware.js';
8
+ import { invokeControllerHandler } from './dispatch-handler-policy.js';
9
+ import { resolveContentNegotiation, writeErrorResponse, writeSuccessResponse } from './dispatch-response-policy.js';
10
+ import { matchHandlerOrThrow, updateRequestParams } from './dispatch-routing-policy.js';
11
+ import { attachFrameworkRequestNativeRouteHandoff, readFrameworkRequestNativeRouteHandoff } from './native-route-handoff.js';
12
+ import { compileFastPathEligibility, getHandlerFastPathEligibility, setHandlerFastPathEligibility, FAST_PATH_STATS_SYMBOL, addPathDebugHeader, createFastPathStats, createPathDebugInfo, executeFastPath, shouldUseFastPathForRequest } from './fast-path/index.js';
13
+ export { FAST_PATH_ELIGIBILITY_SYMBOL, FAST_PATH_STATS_SYMBOL } from './fast-path/index.js';
10
14
 
11
- /**
12
- * Type definition for a global HTTP error handler function.
13
- */
15
+ /** Type definition for a global HTTP error handler function. */
14
16
 
15
- /**
16
- * Options for creating an HTTP {@link Dispatcher}.
17
- */
17
+ /** Options for creating an HTTP {@link Dispatcher}. */
18
18
 
19
+ const EMPTY_NATIVE_FAST_PATH_HANDLER_EXECUTION_PLANS = new WeakMap();
20
+ const EMPTY_NATIVE_FAST_PATH_OBSERVERS = [];
19
21
  function logDispatchFailure(logger, message, error) {
20
22
  if (logger) {
21
23
  logger.error(message, error, 'HttpDispatcher');
@@ -24,33 +26,260 @@ function logDispatchFailure(logger, message, error) {
24
26
  console.error(`[fluo][HttpDispatcher] ${message}`, error);
25
27
  }
26
28
  function createDispatchRequest(request) {
27
- return {
28
- ...request,
29
+ const dispatchRequest = {
30
+ get cookies() {
31
+ return request.cookies;
32
+ },
33
+ get headers() {
34
+ return request.headers;
35
+ },
36
+ get query() {
37
+ return request.query;
38
+ },
39
+ body: request.body,
40
+ method: request.method,
29
41
  params: {
30
42
  ...request.params
43
+ },
44
+ path: request.path,
45
+ raw: request.raw,
46
+ rawBody: request.rawBody,
47
+ requestId: request.requestId,
48
+ signal: request.signal,
49
+ url: request.url
50
+ };
51
+ const nativeRouteHandoff = readFrameworkRequestNativeRouteHandoff(request);
52
+ const files = request.files;
53
+ const principal = request.principal;
54
+ if (files !== undefined) {
55
+ dispatchRequest.files = files;
56
+ }
57
+ if (principal !== undefined) {
58
+ dispatchRequest.principal = principal;
59
+ }
60
+ return nativeRouteHandoff ? attachFrameworkRequestNativeRouteHandoff(dispatchRequest, nativeRouteHandoff) : dispatchRequest;
61
+ }
62
+ function cloneHandlerDescriptor(descriptor) {
63
+ const cloned = {
64
+ ...descriptor,
65
+ metadata: {
66
+ ...descriptor.metadata,
67
+ moduleMiddleware: [...descriptor.metadata.moduleMiddleware],
68
+ pathParams: [...descriptor.metadata.pathParams]
69
+ },
70
+ route: {
71
+ ...descriptor.route,
72
+ guards: descriptor.route.guards ? [...descriptor.route.guards] : undefined,
73
+ headers: descriptor.route.headers?.map(header => ({
74
+ ...header
75
+ })),
76
+ interceptors: descriptor.route.interceptors ? [...descriptor.route.interceptors] : undefined,
77
+ produces: descriptor.route.produces ? [...descriptor.route.produces] : undefined,
78
+ redirect: descriptor.route.redirect ? {
79
+ ...descriptor.route.redirect
80
+ } : undefined
31
81
  }
32
82
  };
83
+ const eligibility = getHandlerFastPathEligibility(descriptor);
84
+ if (eligibility) {
85
+ setHandlerFastPathEligibility(cloned, eligibility);
86
+ }
87
+ return cloned;
33
88
  }
34
89
  function readRequestId(request) {
90
+ if (request.requestId) {
91
+ return request.requestId;
92
+ }
35
93
  const raw = request.headers['x-request-id'] ?? request.headers['X-Request-Id'];
36
94
  const value = Array.isArray(raw) ? raw[0] : raw;
37
95
  const normalized = value?.trim();
38
96
  return normalized ? normalized : undefined;
39
97
  }
40
- function createDispatchContext(request, response, rootContainer) {
41
- return createRequestContext({
42
- container: rootContainer.createRequestScope(),
98
+ function createDispatchContext(request, response, container, promoteOnContainerAccess) {
99
+ const context = createRequestContext({
100
+ container,
43
101
  metadata: {},
44
102
  request,
45
103
  requestId: readRequestId(request),
46
104
  response
47
105
  });
106
+ if (!promoteOnContainerAccess) {
107
+ return context;
108
+ }
109
+
110
+ // Wrap the container to only promote to request scope when resolve() is actually called.
111
+ // This allows fast-path handlers to check ctx.container without triggering scope creation.
112
+ let activeContainer = container;
113
+ let wrappedContainer;
114
+ let promoted = false;
115
+ const ensurePromoted = () => {
116
+ if (!promoted) {
117
+ activeContainer = promoteOnContainerAccess();
118
+ promoted = true;
119
+ }
120
+ return activeContainer;
121
+ };
122
+ const getWrappedContainer = () => {
123
+ if (!wrappedContainer) {
124
+ wrappedContainer = {
125
+ async resolve(token) {
126
+ const targetContainer = ensurePromoted();
127
+ return targetContainer.resolve(token);
128
+ },
129
+ async dispose() {
130
+ // If promotion never happened, this is a no-op.
131
+ // This prevents accidentally disposing the root container when a
132
+ // captured container reference is used after a singleton-only request.
133
+ if (!promoted) {
134
+ return;
135
+ }
136
+ return activeContainer.dispose();
137
+ }
138
+ };
139
+ }
140
+ return wrappedContainer;
141
+ };
142
+ Object.defineProperty(context, 'container', {
143
+ configurable: true,
144
+ enumerable: true,
145
+ get() {
146
+ // If promotion has already occurred, return the actual container.
147
+ if (promoted) {
148
+ return activeContainer;
149
+ }
150
+ // Return the wrapped container that will promote on resolve().
151
+ return getWrappedContainer();
152
+ },
153
+ set(value) {
154
+ activeContainer = value;
155
+ promoted = true;
156
+ }
157
+ });
158
+ return context;
159
+ }
160
+ function createRootDispatchScope(rootContainer) {
161
+ return {
162
+ container: rootContainer,
163
+ requestScoped: false
164
+ };
165
+ }
166
+ function createRequestDispatchScope(rootContainer) {
167
+ return {
168
+ container: rootContainer.createRequestScope(),
169
+ requestScoped: true
170
+ };
171
+ }
172
+ function activeMiddlewareMayRequireRequestScope(definitions, request) {
173
+ return definitions.some(definition => {
174
+ if (!isMiddlewareRouteConfig(definition)) {
175
+ return true;
176
+ }
177
+ return definition.routes.length === 0 || definition.routes.some(route => matchRoutePattern(route, request.path));
178
+ });
179
+ }
180
+ function compileMiddlewareScopePlan(definitions) {
181
+ const conditionalDefinitions = [];
182
+ for (const definition of definitions) {
183
+ if (!isMiddlewareRouteConfig(definition) || definition.routes.length === 0) {
184
+ return {
185
+ alwaysRequiresRequestScope: true,
186
+ conditionalDefinitions: []
187
+ };
188
+ }
189
+ conditionalDefinitions.push(definition);
190
+ }
191
+ return {
192
+ alwaysRequiresRequestScope: false,
193
+ conditionalDefinitions
194
+ };
195
+ }
196
+ function compiledMiddlewareMayRequireRequestScope(plan, request) {
197
+ return plan.alwaysRequiresRequestScope || activeMiddlewareMayRequireRequestScope(plan.conditionalDefinitions, request);
198
+ }
199
+ function requestDtoMayRequireRequestScope(handler, options) {
200
+ if (!handler.route.request) {
201
+ return false;
202
+ }
203
+ if ((options.requestScope?.converterDefinitions ?? []).length > 0) {
204
+ return true;
205
+ }
206
+ if (options.binder) {
207
+ return true;
208
+ }
209
+ const plan = getCompiledDtoBindingPlan(handler.route.request);
210
+ return plan.entries.some(entry => entry.converter !== undefined);
211
+ }
212
+ function handlerMethodMayUseRequestContext(handler) {
213
+ const method = handler.controllerToken.prototype[handler.methodName];
214
+ return typeof method === 'function' && method.length >= 2;
215
+ }
216
+ function hasRequestScopeInspector(container) {
217
+ return typeof container === 'object' && container !== null && 'hasRequestScopedDependency' in container && typeof container.hasRequestScopedDependency === 'function';
218
+ }
219
+ function compileHandlerExecutionPlan(handler, options) {
220
+ const routeGuards = handler.route.guards ?? [];
221
+ const requestScope = compileMiddlewareScopePlan(handler.metadata.moduleMiddleware);
222
+ const mergedInterceptors = mergeInterceptors(options.interceptors ?? [], handler.route.interceptors ?? []);
223
+ return {
224
+ mergedInterceptors,
225
+ requestScope,
226
+ requiresRequestScope: routeGuards.length > 0 || mergedInterceptors.length > 0 || requestScope.alwaysRequiresRequestScope || requestDtoMayRequireRequestScope(handler, options) || handlerMethodMayUseRequestContext(handler) || (hasRequestScopeInspector(options.rootContainer) ? options.rootContainer.hasRequestScopedDependency(handler.controllerToken) : true),
227
+ routeGuards
228
+ };
229
+ }
230
+ function handlerMayRequireRequestScope(plan, request) {
231
+ return plan.requiresRequestScope || compiledMiddlewareMayRequireRequestScope(plan.requestScope, request);
232
+ }
233
+ function compileDispatchStartPlan(observers, appMiddleware) {
234
+ const requestScope = compileMiddlewareScopePlan(appMiddleware);
235
+ return {
236
+ requestScope,
237
+ requiresRequestScope: observers.length > 0 || requestScope.alwaysRequiresRequestScope
238
+ };
239
+ }
240
+ function dispatchStartMayRequireRequestScope(plan, request) {
241
+ return plan.requiresRequestScope || compiledMiddlewareMayRequireRequestScope(plan.requestScope, request);
242
+ }
243
+ function ensureRequestScope(context) {
244
+ if (context.dispatchScope.requestScoped) {
245
+ return;
246
+ }
247
+ context.dispatchScope = createRequestDispatchScope(context.options.rootContainer);
248
+ context.requestContext.container = context.dispatchScope.container;
48
249
  }
49
250
  function ensureRequestNotAborted(request) {
50
- if (request.signal?.aborted) {
251
+ if (isRequestAborted(request)) {
51
252
  throw new RequestAbortedError();
52
253
  }
53
254
  }
255
+ function isRequestAborted(request) {
256
+ return request.isAborted?.() ?? request.signal?.aborted === true;
257
+ }
258
+ function resolveFastPathHandlerRuntimeCache(handler, cache) {
259
+ const cached = cache.get(handler);
260
+ if (cached) {
261
+ return cached;
262
+ }
263
+ const method = handler.controllerToken.prototype[handler.methodName];
264
+ const compiled = {
265
+ method: typeof method === 'function' ? method : undefined
266
+ };
267
+ cache.set(handler, compiled);
268
+ return compiled;
269
+ }
270
+ function resolveFastPathController(handler, controllerContainer, runtimeCache) {
271
+ if (runtimeCache.controller) {
272
+ return runtimeCache.controller;
273
+ }
274
+ runtimeCache.controllerPromise ??= controllerContainer.resolve(handler.controllerToken).then(controller => {
275
+ runtimeCache.controller = controller;
276
+ return controller;
277
+ });
278
+ return runtimeCache.controllerPromise;
279
+ }
280
+ function isPromiseLike(value) {
281
+ return typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function';
282
+ }
54
283
  function isRequestObserver(value) {
55
284
  return typeof value === 'object' && value !== null;
56
285
  }
@@ -71,29 +300,40 @@ async function notifyObservers(observers, requestContext, callback, handler) {
71
300
  }
72
301
  }
73
302
  async function notifyObserversSafely(observers, requestContext, callback, logger, handler) {
303
+ if (observers.length === 0) {
304
+ return;
305
+ }
74
306
  try {
75
307
  await notifyObservers(observers, requestContext, callback, handler);
76
308
  } catch (error) {
77
309
  logDispatchFailure(logger, 'Request observer threw an unhandled error.', error);
78
310
  }
79
311
  }
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);
312
+ function mergeInterceptors(globalInterceptors, routeInterceptors) {
313
+ if (globalInterceptors.length === 0) {
314
+ return routeInterceptors;
315
+ }
316
+ if (routeInterceptors.length === 0) {
317
+ return globalInterceptors;
318
+ }
319
+ return [...globalInterceptors, ...routeInterceptors];
320
+ }
321
+ async function dispatchMatchedHandler(handler, executionPlan, requestContext, controllerContainer, observers, contentNegotiation, binder, logger) {
322
+ const routeGuards = executionPlan.routeGuards;
323
+ if (routeGuards.length > 0) {
324
+ const guardContext = {
325
+ handler,
326
+ requestContext
327
+ };
328
+ await runGuardChain(routeGuards, guardContext);
329
+ }
90
330
  if (requestContext.response.committed) {
91
331
  return;
92
332
  }
93
- const interceptors = [...(globalInterceptors ?? []), ...(handler.route.interceptors ?? [])];
94
- const result = await runInterceptorChain(interceptors, interceptorContext, async () => {
95
- return invokeControllerHandler(handler, requestContext, binder);
96
- });
333
+ const result = executionPlan.mergedInterceptors.length === 0 ? await invokeControllerHandler(handler, requestContext, binder, controllerContainer) : await runInterceptorChain(executionPlan.mergedInterceptors, {
334
+ handler,
335
+ requestContext
336
+ }, async () => invokeControllerHandler(handler, requestContext, binder, controllerContainer));
97
337
  ensureRequestNotAborted(requestContext.request);
98
338
  if (!(result instanceof SseResponse) && !requestContext.response.committed) {
99
339
  await writeSuccessResponse(handler, requestContext.request, requestContext.response, result, contentNegotiation);
@@ -102,6 +342,72 @@ async function dispatchMatchedHandler(handler, requestContext, observers, conten
102
342
  await observer.onRequestSuccess?.(context, result);
103
343
  }, logger, handler);
104
344
  }
345
+ function resolveHandlerExecutionPlan(handler, executionPlans, options) {
346
+ const cached = executionPlans.get(handler);
347
+ if (cached) {
348
+ return cached;
349
+ }
350
+ const compiled = compileHandlerExecutionPlan(handler, options);
351
+ executionPlans.set(handler, compiled);
352
+ return compiled;
353
+ }
354
+ async function dispatchNativeFastRoute(match, request, response, options, contentNegotiation, fastPathRuntimeCache) {
355
+ const eligibility = getHandlerFastPathEligibility(match.descriptor);
356
+ if (!shouldUseFastPathForRequest(eligibility, request)) {
357
+ return false;
358
+ }
359
+ if (options.fastPathDebugHeaders === true && eligibility && !response.committed) {
360
+ const debugInfo = createPathDebugInfo(eligibility);
361
+ addPathDebugHeader(response.setHeader.bind(response), debugInfo);
362
+ }
363
+ const dispatchRequest = request;
364
+ const dispatchScope = createRootDispatchScope(options.rootContainer);
365
+ let phaseContext;
366
+ let containerPromotionOpen = true;
367
+ const requestContext = createDispatchContext(dispatchRequest, response, dispatchScope.container, () => {
368
+ if (!containerPromotionOpen) {
369
+ return phaseContext.dispatchScope.container;
370
+ }
371
+ ensureRequestScope(phaseContext);
372
+ return phaseContext.dispatchScope.container;
373
+ });
374
+ phaseContext = {
375
+ contentNegotiation,
376
+ dispatchScope,
377
+ fastPathRuntimeCache,
378
+ handlerExecutionPlans: EMPTY_NATIVE_FAST_PATH_HANDLER_EXECUTION_PLANS,
379
+ observers: EMPTY_NATIVE_FAST_PATH_OBSERVERS,
380
+ options,
381
+ requestContext,
382
+ response
383
+ };
384
+ phaseContext.matchedHandler = match.descriptor;
385
+ updateRequestParams(phaseContext.requestContext, match.params);
386
+ await runWithRequestContext(phaseContext.requestContext, async () => {
387
+ try {
388
+ ensureRequestNotAborted(phaseContext.requestContext.request);
389
+ const fastPathSuccess = await tryFastPathExecution(match.descriptor, phaseContext);
390
+ if (!fastPathSuccess) {
391
+ throw new Error(`Native route ${match.descriptor.route.method}:${match.descriptor.route.path} was not fast-path executable.`);
392
+ }
393
+ } catch (error) {
394
+ await handleDispatchError(phaseContext, error);
395
+ } finally {
396
+ if (!phaseContext.dispatchScope.requestScoped) {
397
+ phaseContext.requestContext.container = phaseContext.dispatchScope.container;
398
+ }
399
+ containerPromotionOpen = false;
400
+ if (phaseContext.dispatchScope.requestScoped) {
401
+ try {
402
+ await phaseContext.dispatchScope.container.dispose();
403
+ } catch (error) {
404
+ logDispatchFailure(options.logger, 'Request-scoped container dispose threw an error.', error);
405
+ }
406
+ }
407
+ }
408
+ });
409
+ return true;
410
+ }
105
411
  async function notifyRequestStart(context) {
106
412
  await notifyObserversSafely(context.observers, context.requestContext, async (observer, observationContext) => {
107
413
  await observer.onRequestStart?.(observationContext);
@@ -122,6 +428,36 @@ async function notifyRequestFinish(context) {
122
428
  await observer.onRequestFinish?.(observationContext);
123
429
  }, context.options.logger, context.matchedHandler);
124
430
  }
431
+ async function tryFastPathExecution(handler, context) {
432
+ const eligibility = getHandlerFastPathEligibility(handler);
433
+ if (!eligibility || eligibility.executionPath !== 'fast') {
434
+ return false;
435
+ }
436
+ if (typeof context.dispatchScope.container.resolve !== 'function') {
437
+ ensureRequestScope(context);
438
+ }
439
+ const runtimeCache = resolveFastPathHandlerRuntimeCache(handler, context.fastPathRuntimeCache);
440
+ const controllerOrPromise = resolveFastPathController(handler, context.dispatchScope.container, runtimeCache);
441
+ const controller = isPromiseLike(controllerOrPromise) ? await controllerOrPromise : controllerOrPromise;
442
+ const fastPathResult = await executeFastPath({
443
+ binder: context.options.binder,
444
+ contentNegotiation: context.contentNegotiation,
445
+ controller,
446
+ controllerContainer: context.dispatchScope.container,
447
+ handler,
448
+ method: runtimeCache.method,
449
+ request: context.requestContext.request,
450
+ requestContext: context.requestContext,
451
+ response: context.response
452
+ });
453
+ if (fastPathResult.executed) {
454
+ return true;
455
+ }
456
+ if (fastPathResult.error) {
457
+ throw fastPathResult.error;
458
+ }
459
+ return false;
460
+ }
125
461
  async function runDispatchPipeline(context) {
126
462
  ensureRequestNotAborted(context.requestContext.request);
127
463
  const appMiddlewareContext = {
@@ -129,13 +465,28 @@ async function runDispatchPipeline(context) {
129
465
  requestContext: context.requestContext,
130
466
  response: context.response
131
467
  };
132
- await runMiddlewareChain(context.options.appMiddleware ?? [], appMiddlewareContext, async () => {
468
+ const dispatchMatchedRoute = async () => {
133
469
  if (context.response.committed) {
134
470
  return;
135
471
  }
136
- const match = matchHandlerOrThrow(context.options.handlerMapping, appMiddlewareContext.request);
472
+ const match = readFrameworkRequestNativeRouteHandoff(appMiddlewareContext.request) ?? matchHandlerOrThrow(context.options.handlerMapping, appMiddlewareContext.request);
137
473
  context.matchedHandler = match.descriptor;
138
474
  updateRequestParams(context.requestContext, match.params);
475
+ const eligibility = getHandlerFastPathEligibility(match.descriptor);
476
+ if (context.options.fastPathDebugHeaders === true && eligibility && !context.response.committed) {
477
+ const debugInfo = createPathDebugInfo(eligibility);
478
+ addPathDebugHeader(context.response.setHeader.bind(context.response), debugInfo);
479
+ }
480
+ if (shouldUseFastPathForRequest(eligibility, appMiddlewareContext.request)) {
481
+ const fastPathSuccess = await tryFastPathExecution(match.descriptor, context);
482
+ if (fastPathSuccess) {
483
+ return;
484
+ }
485
+ }
486
+ const executionPlan = resolveHandlerExecutionPlan(match.descriptor, context.handlerExecutionPlans, context.options);
487
+ if (handlerMayRequireRequestScope(executionPlan, appMiddlewareContext.request)) {
488
+ ensureRequestScope(context);
489
+ }
139
490
  await notifyHandlerMatched(context, match.descriptor);
140
491
  const moduleMiddlewareContext = {
141
492
  request: context.requestContext.request,
@@ -143,12 +494,18 @@ async function runDispatchPipeline(context) {
143
494
  response: context.response
144
495
  };
145
496
  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);
497
+ await dispatchMatchedHandler(match.descriptor, executionPlan, context.requestContext, context.dispatchScope.container, context.observers, context.contentNegotiation, context.options.binder, context.options.logger);
147
498
  });
148
- });
499
+ };
500
+ const appMiddleware = context.options.appMiddleware ?? [];
501
+ if (appMiddleware.length === 0) {
502
+ await dispatchMatchedRoute();
503
+ return;
504
+ }
505
+ await runMiddlewareChain(appMiddleware, appMiddlewareContext, dispatchMatchedRoute);
149
506
  }
150
507
  async function handleDispatchError(context, error) {
151
- if (error instanceof RequestAbortedError || context.requestContext.request.signal?.aborted) {
508
+ if (error instanceof RequestAbortedError || isRequestAborted(context.requestContext.request)) {
152
509
  return;
153
510
  }
154
511
  await notifyRequestError(context, error);
@@ -167,30 +524,89 @@ async function handleDispatchError(context, error) {
167
524
  */
168
525
  export function createDispatcher(options) {
169
526
  const contentNegotiation = resolveContentNegotiation(options.contentNegotiation);
170
- return {
527
+ const observers = options.observers ?? [];
528
+ const appMiddleware = options.appMiddleware ?? [];
529
+ const dispatchStartPlan = compileDispatchStartPlan(observers, appMiddleware);
530
+ const fastPathRuntimeCache = new WeakMap();
531
+ const handlerExecutionPlans = new WeakMap();
532
+ const adapter = options.adapter ?? 'default';
533
+ const fastPathEligibilities = [];
534
+ for (const descriptor of options.handlerMapping.descriptors) {
535
+ handlerExecutionPlans.set(descriptor, compileHandlerExecutionPlan(descriptor, options));
536
+ const {
537
+ eligibility
538
+ } = compileFastPathEligibility(descriptor, options, adapter);
539
+ setHandlerFastPathEligibility(descriptor, eligibility);
540
+ fastPathEligibilities.push(eligibility);
541
+ }
542
+ const fastPathStats = createFastPathStats(fastPathEligibilities);
543
+ const dispatcher = {
544
+ describeRoutes() {
545
+ return options.handlerMapping.descriptors.map(descriptor => cloneHandlerDescriptor(descriptor));
546
+ },
547
+ async dispatchNativeRoute(match, request, response) {
548
+ return dispatchNativeFastRoute(match, request, response, options, contentNegotiation, fastPathRuntimeCache);
549
+ },
171
550
  async dispatch(request, response) {
172
- const phaseContext = {
551
+ const dispatchRequest = createDispatchRequest(request);
552
+ const dispatchScope = dispatchStartMayRequireRequestScope(dispatchStartPlan, dispatchRequest) ? createRequestDispatchScope(options.rootContainer) : createRootDispatchScope(options.rootContainer);
553
+ let phaseContext;
554
+ let containerPromotionOpen = true;
555
+ const requestContext = createDispatchContext(dispatchRequest, response, dispatchScope.container, () => {
556
+ if (!containerPromotionOpen) {
557
+ return phaseContext.dispatchScope.container;
558
+ }
559
+ ensureRequestScope(phaseContext);
560
+ return phaseContext.dispatchScope.container;
561
+ });
562
+ phaseContext = {
173
563
  contentNegotiation,
174
- observers: options.observers ?? [],
564
+ dispatchScope,
565
+ fastPathRuntimeCache,
566
+ handlerExecutionPlans,
567
+ observers,
175
568
  options,
176
- requestContext: createDispatchContext(createDispatchRequest(request), response, options.rootContainer),
569
+ requestContext,
177
570
  response
178
571
  };
179
572
  await runWithRequestContext(phaseContext.requestContext, async () => {
180
573
  try {
181
- await notifyRequestStart(phaseContext);
574
+ if (observers.length > 0) {
575
+ await notifyRequestStart(phaseContext);
576
+ }
182
577
  await runDispatchPipeline(phaseContext);
183
578
  } catch (error) {
184
579
  await handleDispatchError(phaseContext, error);
185
580
  } finally {
186
- 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);
581
+ if (observers.length > 0) {
582
+ await notifyRequestFinish(phaseContext);
583
+ }
584
+ if (!phaseContext.dispatchScope.requestScoped) {
585
+ phaseContext.requestContext.container = phaseContext.dispatchScope.container;
586
+ }
587
+ containerPromotionOpen = false;
588
+ if (phaseContext.dispatchScope.requestScoped) {
589
+ try {
590
+ await phaseContext.dispatchScope.container.dispose();
591
+ } catch (error) {
592
+ logDispatchFailure(options.logger, 'Request-scoped container dispose threw an error.', error);
593
+ }
191
594
  }
192
595
  }
193
596
  });
194
597
  }
195
598
  };
196
- }
599
+ dispatcher[FAST_PATH_STATS_SYMBOL] = fastPathStats;
600
+ return dispatcher;
601
+ }
602
+
603
+ /**
604
+ * Reads automatic fast-path eligibility statistics attached to a dispatcher.
605
+ *
606
+ * @param dispatcher Dispatcher returned by {@link createDispatcher}.
607
+ * @returns Fast-path statistics when available.
608
+ */
609
+ export function getDispatcherFastPathStats(dispatcher) {
610
+ return dispatcher[FAST_PATH_STATS_SYMBOL];
611
+ }
612
+ export { formatFastPathStats } from './fast-path/index.js';
@@ -0,0 +1,18 @@
1
+ import type { FastPathEligibility, FastPathStats } from './eligibility.js';
2
+ interface PathDebugInfo {
3
+ executionPath: 'fast' | 'full';
4
+ fallbackReason?: string;
5
+ routeId: string;
6
+ }
7
+ export declare function createPathDebugInfo(eligibility: FastPathEligibility): PathDebugInfo;
8
+ export declare function addPathDebugHeader(setHeader: (name: string, value: string) => void, info: PathDebugInfo): void;
9
+ export declare function createFastPathStats(eligibilities: readonly FastPathEligibility[]): FastPathStats;
10
+ /**
11
+ * Formats dispatcher fast-path statistics for debug logs and benchmark output.
12
+ *
13
+ * @param stats Fast-path statistics returned by {@link getDispatcherFastPathStats}.
14
+ * @returns A human-readable route breakdown.
15
+ */
16
+ export declare function formatFastPathStats(stats: FastPathStats): string;
17
+ export {};
18
+ //# sourceMappingURL=debug-visibility.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debug-visibility.d.ts","sourceRoot":"","sources":["../../../src/dispatch/fast-path/debug-visibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAI3E,UAAU,aAAa;IACrB,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,mBAAmB,GAAG,aAAa,CAMnF;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,EAChD,IAAI,EAAE,aAAa,GAClB,IAAI,CAMN;AAED,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,SAAS,mBAAmB,EAAE,GAAG,aAAa,CAShG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAuBhE"}
@@ -0,0 +1,39 @@
1
+ const DEBUG_HEADER_NAME = 'X-Fluo-Path';
2
+ export function createPathDebugInfo(eligibility) {
3
+ return {
4
+ executionPath: eligibility.executionPath,
5
+ fallbackReason: eligibility.fallbackReason,
6
+ routeId: eligibility.routeId
7
+ };
8
+ }
9
+ export function addPathDebugHeader(setHeader, info) {
10
+ const value = info.executionPath === 'fast' ? `fast; route=${info.routeId}` : `full; route=${info.routeId}; reason=${info.fallbackReason ?? 'none'}`;
11
+ setHeader(DEBUG_HEADER_NAME, value);
12
+ }
13
+ export function createFastPathStats(eligibilities) {
14
+ const fastPathRoutes = eligibilities.filter(e => e.executionPath === 'fast').length;
15
+ return {
16
+ fastPathRoutes,
17
+ fullPathRoutes: eligibilities.length - fastPathRoutes,
18
+ routes: eligibilities,
19
+ totalRoutes: eligibilities.length
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Formats dispatcher fast-path statistics for debug logs and benchmark output.
25
+ *
26
+ * @param stats Fast-path statistics returned by {@link getDispatcherFastPathStats}.
27
+ * @returns A human-readable route breakdown.
28
+ */
29
+ export function formatFastPathStats(stats) {
30
+ const fastPathPercent = stats.totalRoutes === 0 ? '0.0' : (stats.fastPathRoutes / stats.totalRoutes * 100).toFixed(1);
31
+ const fullPathPercent = stats.totalRoutes === 0 ? '0.0' : (stats.fullPathRoutes / stats.totalRoutes * 100).toFixed(1);
32
+ const lines = ['=== Fast Path Statistics ===', `Total routes: ${String(stats.totalRoutes)}`, `Fast path: ${String(stats.fastPathRoutes)} (${fastPathPercent}%)`, `Full path: ${String(stats.fullPathRoutes)} (${fullPathPercent}%)`, '', 'Route breakdown:'];
33
+ for (const route of stats.routes) {
34
+ const status = route.executionPath === 'fast' ? 'FAST' : 'FULL';
35
+ const reason = route.fallbackReason ? ` (${route.fallbackReason})` : '';
36
+ lines.push(` [${status}] ${route.routeId}${reason}`);
37
+ }
38
+ return lines.join('\n');
39
+ }