@fluojs/http 1.0.0-beta.4 → 1.0.0-beta.6

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 (41) hide show
  1. package/dist/adapters/binding.d.ts.map +1 -1
  2. package/dist/adapters/binding.js +37 -7
  3. package/dist/adapters/dto-binding-plan.d.ts +3 -0
  4. package/dist/adapters/dto-binding-plan.d.ts.map +1 -1
  5. package/dist/adapters/dto-binding-plan.js +34 -2
  6. package/dist/adapters/dto-validation-adapter.d.ts +1 -1
  7. package/dist/adapters/dto-validation-adapter.d.ts.map +1 -1
  8. package/dist/adapters/dto-validation-adapter.js +7 -14
  9. package/dist/dispatch/dispatch-handler-policy.d.ts.map +1 -1
  10. package/dist/dispatch/dispatch-handler-policy.js +5 -3
  11. package/dist/dispatch/dispatch-response-policy.d.ts +1 -1
  12. package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -1
  13. package/dist/dispatch/dispatch-response-policy.js +3 -4
  14. package/dist/dispatch/dispatch-routing-policy.d.ts.map +1 -1
  15. package/dist/dispatch/dispatch-routing-policy.js +6 -4
  16. package/dist/dispatch/dispatcher.d.ts +18 -6
  17. package/dist/dispatch/dispatcher.d.ts.map +1 -1
  18. package/dist/dispatch/dispatcher.js +317 -52
  19. package/dist/dispatch/fast-path/debug-visibility.d.ts +18 -0
  20. package/dist/dispatch/fast-path/debug-visibility.d.ts.map +1 -0
  21. package/dist/dispatch/fast-path/debug-visibility.js +39 -0
  22. package/dist/dispatch/fast-path/eligibility-checker.d.ts +22 -0
  23. package/dist/dispatch/fast-path/eligibility-checker.d.ts.map +1 -0
  24. package/dist/dispatch/fast-path/eligibility-checker.js +110 -0
  25. package/dist/dispatch/fast-path/eligibility.d.ts +61 -0
  26. package/dist/dispatch/fast-path/eligibility.d.ts.map +1 -0
  27. package/dist/dispatch/fast-path/eligibility.js +23 -0
  28. package/dist/dispatch/fast-path/fast-path-executor.d.ts +21 -0
  29. package/dist/dispatch/fast-path/fast-path-executor.d.ts.map +1 -0
  30. package/dist/dispatch/fast-path/fast-path-executor.js +80 -0
  31. package/dist/dispatch/fast-path/index.d.ts +6 -0
  32. package/dist/dispatch/fast-path/index.d.ts.map +1 -0
  33. package/dist/dispatch/fast-path/index.js +4 -0
  34. package/dist/dispatch/native-route-handoff.d.ts.map +1 -1
  35. package/dist/dispatch/native-route-handoff.js +8 -5
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -0
  39. package/dist/types.d.ts +12 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +1 -1
@@ -1,23 +1,23 @@
1
- import { readFrameworkRequestNativeRouteHandoff } from './native-route-handoff.js';
2
- import { invokeControllerHandler } from './dispatch-handler-policy.js';
3
- import { resolveContentNegotiation, writeErrorResponse, writeSuccessResponse } from './dispatch-response-policy.js';
4
- import { matchHandlerOrThrow, updateRequestParams } from './dispatch-routing-policy.js';
5
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';
6
4
  import { RequestAbortedError } from '../errors.js';
7
5
  import { runGuardChain } from '../guards.js';
8
6
  import { runInterceptorChain } from '../interceptors.js';
9
7
  import { isMiddlewareRouteConfig, matchRoutePattern, runMiddlewareChain } from '../middleware/middleware.js';
10
- import { createRequestContext, runWithRequestContext } from '../context/request-context.js';
11
- import { SseResponse } from '../context/sse.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';
12
14
 
13
- /**
14
- * Type definition for a global HTTP error handler function.
15
- */
15
+ /** Type definition for a global HTTP error handler function. */
16
16
 
17
- /**
18
- * Options for creating an HTTP {@link Dispatcher}.
19
- */
17
+ /** Options for creating an HTTP {@link Dispatcher}. */
20
18
 
19
+ const EMPTY_NATIVE_FAST_PATH_HANDLER_EXECUTION_PLANS = new WeakMap();
20
+ const EMPTY_NATIVE_FAST_PATH_OBSERVERS = [];
21
21
  function logDispatchFailure(logger, message, error) {
22
22
  if (logger) {
23
23
  logger.error(message, error, 'HttpDispatcher');
@@ -26,15 +26,37 @@ function logDispatchFailure(logger, message, error) {
26
26
  console.error(`[fluo][HttpDispatcher] ${message}`, error);
27
27
  }
28
28
  function createDispatchRequest(request) {
29
- return {
30
- ...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,
31
41
  params: {
32
42
  ...request.params
33
- }
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
34
50
  };
51
+ const nativeRouteHandoff = readFrameworkRequestNativeRouteHandoff(request);
52
+ const files = request.files;
53
+ if (files !== undefined) {
54
+ dispatchRequest.files = files;
55
+ }
56
+ return nativeRouteHandoff ? attachFrameworkRequestNativeRouteHandoff(dispatchRequest, nativeRouteHandoff) : dispatchRequest;
35
57
  }
36
58
  function cloneHandlerDescriptor(descriptor) {
37
- return {
59
+ const cloned = {
38
60
  ...descriptor,
39
61
  metadata: {
40
62
  ...descriptor.metadata,
@@ -54,8 +76,16 @@ function cloneHandlerDescriptor(descriptor) {
54
76
  } : undefined
55
77
  }
56
78
  };
79
+ const eligibility = getHandlerFastPathEligibility(descriptor);
80
+ if (eligibility) {
81
+ setHandlerFastPathEligibility(cloned, eligibility);
82
+ }
83
+ return cloned;
57
84
  }
58
85
  function readRequestId(request) {
86
+ if (request.requestId) {
87
+ return request.requestId;
88
+ }
59
89
  const raw = request.headers['x-request-id'] ?? request.headers['X-Request-Id'];
60
90
  const value = Array.isArray(raw) ? raw[0] : raw;
61
91
  const normalized = value?.trim();
@@ -72,16 +102,53 @@ function createDispatchContext(request, response, container, promoteOnContainerA
72
102
  if (!promoteOnContainerAccess) {
73
103
  return context;
74
104
  }
105
+
106
+ // Wrap the container to only promote to request scope when resolve() is actually called.
107
+ // This allows fast-path handlers to check ctx.container without triggering scope creation.
75
108
  let activeContainer = container;
109
+ let wrappedContainer;
110
+ let promoted = false;
111
+ const ensurePromoted = () => {
112
+ if (!promoted) {
113
+ activeContainer = promoteOnContainerAccess();
114
+ promoted = true;
115
+ }
116
+ return activeContainer;
117
+ };
118
+ const getWrappedContainer = () => {
119
+ if (!wrappedContainer) {
120
+ wrappedContainer = {
121
+ async resolve(token) {
122
+ const targetContainer = ensurePromoted();
123
+ return targetContainer.resolve(token);
124
+ },
125
+ async dispose() {
126
+ // If promotion never happened, this is a no-op.
127
+ // This prevents accidentally disposing the root container when a
128
+ // captured container reference is used after a singleton-only request.
129
+ if (!promoted) {
130
+ return;
131
+ }
132
+ return activeContainer.dispose();
133
+ }
134
+ };
135
+ }
136
+ return wrappedContainer;
137
+ };
76
138
  Object.defineProperty(context, 'container', {
77
139
  configurable: true,
78
140
  enumerable: true,
79
141
  get() {
80
- activeContainer = promoteOnContainerAccess();
81
- return activeContainer;
142
+ // If promotion has already occurred, return the actual container.
143
+ if (promoted) {
144
+ return activeContainer;
145
+ }
146
+ // Return the wrapped container that will promote on resolve().
147
+ return getWrappedContainer();
82
148
  },
83
149
  set(value) {
84
150
  activeContainer = value;
151
+ promoted = true;
85
152
  }
86
153
  });
87
154
  return context;
@@ -106,6 +173,25 @@ function activeMiddlewareMayRequireRequestScope(definitions, request) {
106
173
  return definition.routes.length === 0 || definition.routes.some(route => matchRoutePattern(route, request.path));
107
174
  });
108
175
  }
176
+ function compileMiddlewareScopePlan(definitions) {
177
+ const conditionalDefinitions = [];
178
+ for (const definition of definitions) {
179
+ if (!isMiddlewareRouteConfig(definition) || definition.routes.length === 0) {
180
+ return {
181
+ alwaysRequiresRequestScope: true,
182
+ conditionalDefinitions: []
183
+ };
184
+ }
185
+ conditionalDefinitions.push(definition);
186
+ }
187
+ return {
188
+ alwaysRequiresRequestScope: false,
189
+ conditionalDefinitions
190
+ };
191
+ }
192
+ function compiledMiddlewareMayRequireRequestScope(plan, request) {
193
+ return plan.alwaysRequiresRequestScope || activeMiddlewareMayRequireRequestScope(plan.conditionalDefinitions, request);
194
+ }
109
195
  function requestDtoMayRequireRequestScope(handler, options) {
110
196
  if (!handler.route.request) {
111
197
  return false;
@@ -126,26 +212,29 @@ function handlerMethodMayUseRequestContext(handler) {
126
212
  function hasRequestScopeInspector(container) {
127
213
  return typeof container === 'object' && container !== null && 'hasRequestScopedDependency' in container && typeof container.hasRequestScopedDependency === 'function';
128
214
  }
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;
215
+ function compileHandlerExecutionPlan(handler, options) {
216
+ const routeGuards = handler.route.guards ?? [];
217
+ const requestScope = compileMiddlewareScopePlan(handler.metadata.moduleMiddleware);
218
+ const mergedInterceptors = mergeInterceptors(options.interceptors ?? [], handler.route.interceptors ?? []);
219
+ return {
220
+ mergedInterceptors,
221
+ requestScope,
222
+ requiresRequestScope: routeGuards.length > 0 || mergedInterceptors.length > 0 || requestScope.alwaysRequiresRequestScope || requestDtoMayRequireRequestScope(handler, options) || handlerMethodMayUseRequestContext(handler) || (hasRequestScopeInspector(options.rootContainer) ? options.rootContainer.hasRequestScopedDependency(handler.controllerToken) : true),
223
+ routeGuards
224
+ };
146
225
  }
147
- function dispatchStartMayRequireRequestScope(request, observers, options) {
148
- return observers.length > 0 || activeMiddlewareMayRequireRequestScope(options.appMiddleware ?? [], request);
226
+ function handlerMayRequireRequestScope(plan, request) {
227
+ return plan.requiresRequestScope || compiledMiddlewareMayRequireRequestScope(plan.requestScope, request);
228
+ }
229
+ function compileDispatchStartPlan(observers, appMiddleware) {
230
+ const requestScope = compileMiddlewareScopePlan(appMiddleware);
231
+ return {
232
+ requestScope,
233
+ requiresRequestScope: observers.length > 0 || requestScope.alwaysRequiresRequestScope
234
+ };
235
+ }
236
+ function dispatchStartMayRequireRequestScope(plan, request) {
237
+ return plan.requiresRequestScope || compiledMiddlewareMayRequireRequestScope(plan.requestScope, request);
149
238
  }
150
239
  function ensureRequestScope(context) {
151
240
  if (context.dispatchScope.requestScoped) {
@@ -155,10 +244,38 @@ function ensureRequestScope(context) {
155
244
  context.requestContext.container = context.dispatchScope.container;
156
245
  }
157
246
  function ensureRequestNotAborted(request) {
158
- if (request.signal?.aborted) {
247
+ if (isRequestAborted(request)) {
159
248
  throw new RequestAbortedError();
160
249
  }
161
250
  }
251
+ function isRequestAborted(request) {
252
+ return request.isAborted?.() ?? request.signal?.aborted === true;
253
+ }
254
+ function resolveFastPathHandlerRuntimeCache(handler, cache) {
255
+ const cached = cache.get(handler);
256
+ if (cached) {
257
+ return cached;
258
+ }
259
+ const method = handler.controllerToken.prototype[handler.methodName];
260
+ const compiled = {
261
+ method: typeof method === 'function' ? method : undefined
262
+ };
263
+ cache.set(handler, compiled);
264
+ return compiled;
265
+ }
266
+ function resolveFastPathController(handler, controllerContainer, runtimeCache) {
267
+ if (runtimeCache.controller) {
268
+ return runtimeCache.controller;
269
+ }
270
+ runtimeCache.controllerPromise ??= controllerContainer.resolve(handler.controllerToken).then(controller => {
271
+ runtimeCache.controller = controller;
272
+ return controller;
273
+ });
274
+ return runtimeCache.controllerPromise;
275
+ }
276
+ function isPromiseLike(value) {
277
+ return typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function';
278
+ }
162
279
  function isRequestObserver(value) {
163
280
  return typeof value === 'object' && value !== null;
164
281
  }
@@ -197,8 +314,8 @@ function mergeInterceptors(globalInterceptors, routeInterceptors) {
197
314
  }
198
315
  return [...globalInterceptors, ...routeInterceptors];
199
316
  }
200
- async function dispatchMatchedHandler(handler, requestContext, controllerContainer, observers, contentNegotiation, binder, globalInterceptors, logger) {
201
- const routeGuards = handler.route.guards ?? [];
317
+ async function dispatchMatchedHandler(handler, executionPlan, requestContext, controllerContainer, observers, contentNegotiation, binder, logger) {
318
+ const routeGuards = executionPlan.routeGuards;
202
319
  if (routeGuards.length > 0) {
203
320
  const guardContext = {
204
321
  handler,
@@ -209,8 +326,7 @@ async function dispatchMatchedHandler(handler, requestContext, controllerContain
209
326
  if (requestContext.response.committed) {
210
327
  return;
211
328
  }
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), {
329
+ const result = executionPlan.mergedInterceptors.length === 0 ? await invokeControllerHandler(handler, requestContext, binder, controllerContainer) : await runInterceptorChain(executionPlan.mergedInterceptors, {
214
330
  handler,
215
331
  requestContext
216
332
  }, async () => invokeControllerHandler(handler, requestContext, binder, controllerContainer));
@@ -222,6 +338,68 @@ async function dispatchMatchedHandler(handler, requestContext, controllerContain
222
338
  await observer.onRequestSuccess?.(context, result);
223
339
  }, logger, handler);
224
340
  }
341
+ function resolveHandlerExecutionPlan(handler, executionPlans, options) {
342
+ const cached = executionPlans.get(handler);
343
+ if (cached) {
344
+ return cached;
345
+ }
346
+ const compiled = compileHandlerExecutionPlan(handler, options);
347
+ executionPlans.set(handler, compiled);
348
+ return compiled;
349
+ }
350
+ async function dispatchNativeFastRoute(match, request, response, options, contentNegotiation, fastPathRuntimeCache) {
351
+ const eligibility = getHandlerFastPathEligibility(match.descriptor);
352
+ if (!shouldUseFastPathForRequest(eligibility, request)) {
353
+ return false;
354
+ }
355
+ const dispatchRequest = request;
356
+ const dispatchScope = createRootDispatchScope(options.rootContainer);
357
+ let phaseContext;
358
+ let containerPromotionOpen = true;
359
+ const requestContext = createDispatchContext(dispatchRequest, response, dispatchScope.container, () => {
360
+ if (!containerPromotionOpen) {
361
+ return phaseContext.dispatchScope.container;
362
+ }
363
+ ensureRequestScope(phaseContext);
364
+ return phaseContext.dispatchScope.container;
365
+ });
366
+ phaseContext = {
367
+ contentNegotiation,
368
+ dispatchScope,
369
+ fastPathRuntimeCache,
370
+ handlerExecutionPlans: EMPTY_NATIVE_FAST_PATH_HANDLER_EXECUTION_PLANS,
371
+ observers: EMPTY_NATIVE_FAST_PATH_OBSERVERS,
372
+ options,
373
+ requestContext,
374
+ response
375
+ };
376
+ phaseContext.matchedHandler = match.descriptor;
377
+ updateRequestParams(phaseContext.requestContext, match.params);
378
+ await runWithRequestContext(phaseContext.requestContext, async () => {
379
+ try {
380
+ ensureRequestNotAborted(phaseContext.requestContext.request);
381
+ const fastPathSuccess = await tryFastPathExecution(match.descriptor, phaseContext);
382
+ if (!fastPathSuccess) {
383
+ throw new Error(`Native route ${match.descriptor.route.method}:${match.descriptor.route.path} was not fast-path executable.`);
384
+ }
385
+ } catch (error) {
386
+ await handleDispatchError(phaseContext, error);
387
+ } finally {
388
+ if (!phaseContext.dispatchScope.requestScoped) {
389
+ phaseContext.requestContext.container = phaseContext.dispatchScope.container;
390
+ }
391
+ containerPromotionOpen = false;
392
+ if (phaseContext.dispatchScope.requestScoped) {
393
+ try {
394
+ await phaseContext.dispatchScope.container.dispose();
395
+ } catch (error) {
396
+ logDispatchFailure(options.logger, 'Request-scoped container dispose threw an error.', error);
397
+ }
398
+ }
399
+ }
400
+ });
401
+ return true;
402
+ }
225
403
  async function notifyRequestStart(context) {
226
404
  await notifyObserversSafely(context.observers, context.requestContext, async (observer, observationContext) => {
227
405
  await observer.onRequestStart?.(observationContext);
@@ -242,6 +420,36 @@ async function notifyRequestFinish(context) {
242
420
  await observer.onRequestFinish?.(observationContext);
243
421
  }, context.options.logger, context.matchedHandler);
244
422
  }
423
+ async function tryFastPathExecution(handler, context) {
424
+ const eligibility = getHandlerFastPathEligibility(handler);
425
+ if (!eligibility || eligibility.executionPath !== 'fast') {
426
+ return false;
427
+ }
428
+ if (typeof context.dispatchScope.container.resolve !== 'function') {
429
+ ensureRequestScope(context);
430
+ }
431
+ const runtimeCache = resolveFastPathHandlerRuntimeCache(handler, context.fastPathRuntimeCache);
432
+ const controllerOrPromise = resolveFastPathController(handler, context.dispatchScope.container, runtimeCache);
433
+ const controller = isPromiseLike(controllerOrPromise) ? await controllerOrPromise : controllerOrPromise;
434
+ const fastPathResult = await executeFastPath({
435
+ binder: context.options.binder,
436
+ contentNegotiation: context.contentNegotiation,
437
+ controller,
438
+ controllerContainer: context.dispatchScope.container,
439
+ handler,
440
+ method: runtimeCache.method,
441
+ request: context.requestContext.request,
442
+ requestContext: context.requestContext,
443
+ response: context.response
444
+ });
445
+ if (fastPathResult.executed) {
446
+ return true;
447
+ }
448
+ if (fastPathResult.error) {
449
+ throw fastPathResult.error;
450
+ }
451
+ return false;
452
+ }
245
453
  async function runDispatchPipeline(context) {
246
454
  ensureRequestNotAborted(context.requestContext.request);
247
455
  const appMiddlewareContext = {
@@ -249,16 +457,28 @@ async function runDispatchPipeline(context) {
249
457
  requestContext: context.requestContext,
250
458
  response: context.response
251
459
  };
252
- await runMiddlewareChain(context.options.appMiddleware ?? [], appMiddlewareContext, async () => {
460
+ const dispatchMatchedRoute = async () => {
253
461
  if (context.response.committed) {
254
462
  return;
255
463
  }
256
464
  const match = readFrameworkRequestNativeRouteHandoff(appMiddlewareContext.request) ?? matchHandlerOrThrow(context.options.handlerMapping, appMiddlewareContext.request);
257
465
  context.matchedHandler = match.descriptor;
258
- if (handlerMayRequireRequestScope(match.descriptor, appMiddlewareContext.request, context.options)) {
466
+ updateRequestParams(context.requestContext, match.params);
467
+ const eligibility = getHandlerFastPathEligibility(match.descriptor);
468
+ if (context.options.fastPathDebugHeaders === true && eligibility && !context.response.committed) {
469
+ const debugInfo = createPathDebugInfo(eligibility);
470
+ addPathDebugHeader(context.response.setHeader.bind(context.response), debugInfo);
471
+ }
472
+ if (shouldUseFastPathForRequest(eligibility, appMiddlewareContext.request)) {
473
+ const fastPathSuccess = await tryFastPathExecution(match.descriptor, context);
474
+ if (fastPathSuccess) {
475
+ return;
476
+ }
477
+ }
478
+ const executionPlan = resolveHandlerExecutionPlan(match.descriptor, context.handlerExecutionPlans, context.options);
479
+ if (handlerMayRequireRequestScope(executionPlan, appMiddlewareContext.request)) {
259
480
  ensureRequestScope(context);
260
481
  }
261
- updateRequestParams(context.requestContext, match.params);
262
482
  await notifyHandlerMatched(context, match.descriptor);
263
483
  const moduleMiddlewareContext = {
264
484
  request: context.requestContext.request,
@@ -266,12 +486,18 @@ async function runDispatchPipeline(context) {
266
486
  response: context.response
267
487
  };
268
488
  await runMiddlewareChain(match.descriptor.metadata.moduleMiddleware ?? [], moduleMiddlewareContext, async () => {
269
- await dispatchMatchedHandler(match.descriptor, context.requestContext, context.dispatchScope.container, context.observers, context.contentNegotiation, context.options.binder, context.options.interceptors ?? [], context.options.logger);
489
+ await dispatchMatchedHandler(match.descriptor, executionPlan, context.requestContext, context.dispatchScope.container, context.observers, context.contentNegotiation, context.options.binder, context.options.logger);
270
490
  });
271
- });
491
+ };
492
+ const appMiddleware = context.options.appMiddleware ?? [];
493
+ if (appMiddleware.length === 0) {
494
+ await dispatchMatchedRoute();
495
+ return;
496
+ }
497
+ await runMiddlewareChain(appMiddleware, appMiddlewareContext, dispatchMatchedRoute);
272
498
  }
273
499
  async function handleDispatchError(context, error) {
274
- if (error instanceof RequestAbortedError || context.requestContext.request.signal?.aborted) {
500
+ if (error instanceof RequestAbortedError || isRequestAborted(context.requestContext.request)) {
275
501
  return;
276
502
  }
277
503
  await notifyRequestError(context, error);
@@ -291,13 +517,31 @@ async function handleDispatchError(context, error) {
291
517
  export function createDispatcher(options) {
292
518
  const contentNegotiation = resolveContentNegotiation(options.contentNegotiation);
293
519
  const observers = options.observers ?? [];
520
+ const appMiddleware = options.appMiddleware ?? [];
521
+ const dispatchStartPlan = compileDispatchStartPlan(observers, appMiddleware);
522
+ const fastPathRuntimeCache = new WeakMap();
523
+ const handlerExecutionPlans = new WeakMap();
524
+ const adapter = options.adapter ?? 'default';
525
+ const fastPathEligibilities = [];
526
+ for (const descriptor of options.handlerMapping.descriptors) {
527
+ handlerExecutionPlans.set(descriptor, compileHandlerExecutionPlan(descriptor, options));
528
+ const {
529
+ eligibility
530
+ } = compileFastPathEligibility(descriptor, options, adapter);
531
+ setHandlerFastPathEligibility(descriptor, eligibility);
532
+ fastPathEligibilities.push(eligibility);
533
+ }
534
+ const fastPathStats = createFastPathStats(fastPathEligibilities);
294
535
  const dispatcher = {
295
536
  describeRoutes() {
296
537
  return options.handlerMapping.descriptors.map(descriptor => cloneHandlerDescriptor(descriptor));
297
538
  },
539
+ async dispatchNativeRoute(match, request, response) {
540
+ return dispatchNativeFastRoute(match, request, response, options, contentNegotiation, fastPathRuntimeCache);
541
+ },
298
542
  async dispatch(request, response) {
299
543
  const dispatchRequest = createDispatchRequest(request);
300
- const dispatchScope = dispatchStartMayRequireRequestScope(dispatchRequest, observers, options) ? createRequestDispatchScope(options.rootContainer) : createRootDispatchScope(options.rootContainer);
544
+ const dispatchScope = dispatchStartMayRequireRequestScope(dispatchStartPlan, dispatchRequest) ? createRequestDispatchScope(options.rootContainer) : createRootDispatchScope(options.rootContainer);
301
545
  let phaseContext;
302
546
  let containerPromotionOpen = true;
303
547
  const requestContext = createDispatchContext(dispatchRequest, response, dispatchScope.container, () => {
@@ -310,6 +554,8 @@ export function createDispatcher(options) {
310
554
  phaseContext = {
311
555
  contentNegotiation,
312
556
  dispatchScope,
557
+ fastPathRuntimeCache,
558
+ handlerExecutionPlans,
313
559
  observers,
314
560
  options,
315
561
  requestContext,
@@ -317,12 +563,19 @@ export function createDispatcher(options) {
317
563
  };
318
564
  await runWithRequestContext(phaseContext.requestContext, async () => {
319
565
  try {
320
- await notifyRequestStart(phaseContext);
566
+ if (observers.length > 0) {
567
+ await notifyRequestStart(phaseContext);
568
+ }
321
569
  await runDispatchPipeline(phaseContext);
322
570
  } catch (error) {
323
571
  await handleDispatchError(phaseContext, error);
324
572
  } finally {
325
- await notifyRequestFinish(phaseContext);
573
+ if (observers.length > 0) {
574
+ await notifyRequestFinish(phaseContext);
575
+ }
576
+ if (!phaseContext.dispatchScope.requestScoped) {
577
+ phaseContext.requestContext.container = phaseContext.dispatchScope.container;
578
+ }
326
579
  containerPromotionOpen = false;
327
580
  if (phaseContext.dispatchScope.requestScoped) {
328
581
  try {
@@ -335,5 +588,17 @@ export function createDispatcher(options) {
335
588
  });
336
589
  }
337
590
  };
591
+ dispatcher[FAST_PATH_STATS_SYMBOL] = fastPathStats;
338
592
  return dispatcher;
339
- }
593
+ }
594
+
595
+ /**
596
+ * Reads automatic fast-path eligibility statistics attached to a dispatcher.
597
+ *
598
+ * @param dispatcher Dispatcher returned by {@link createDispatcher}.
599
+ * @returns Fast-path statistics when available.
600
+ */
601
+ export function getDispatcherFastPathStats(dispatcher) {
602
+ return dispatcher[FAST_PATH_STATS_SYMBOL];
603
+ }
604
+ 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
+ }
@@ -0,0 +1,22 @@
1
+ import type { Container } from '@fluojs/di';
2
+ import type { Binder, HandlerDescriptor } from '../../types.js';
3
+ import type { CreateDispatcherOptions } from '../dispatcher.js';
4
+ import { type FastPathEligibility } from './eligibility.js';
5
+ interface CompiledEligibilityPlan {
6
+ eligibility: FastPathEligibility;
7
+ isEligible: boolean;
8
+ }
9
+ export declare function compileFastPathEligibility(handler: HandlerDescriptor, options: CreateDispatcherOptions, adapter: string): CompiledEligibilityPlan;
10
+ export declare function getHandlerFastPathEligibility(handler: HandlerDescriptor): FastPathEligibility | undefined;
11
+ export declare function setHandlerFastPathEligibility(handler: HandlerDescriptor, eligibility: FastPathEligibility): void;
12
+ export interface FastPathExecutorOptions {
13
+ binder?: Binder;
14
+ rootContainer: Container;
15
+ }
16
+ export interface FastPathExecutionResult {
17
+ executed: boolean;
18
+ result?: unknown;
19
+ error?: unknown;
20
+ }
21
+ export {};
22
+ //# sourceMappingURL=eligibility-checker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eligibility-checker.d.ts","sourceRoot":"","sources":["../../../src/dispatch/fast-path/eligibility-checker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,OAAO,KAAK,EACV,MAAM,EACN,iBAAiB,EAElB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,KAAK,mBAAmB,EAAgC,MAAM,kBAAkB,CAAC;AAM1F,UAAU,uBAAuB;IAC/B,WAAW,EAAE,mBAAmB,CAAC;IACjC,UAAU,EAAE,OAAO,CAAC;CACrB;AA0DD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,uBAAuB,EAChC,OAAO,EAAE,MAAM,GACd,uBAAuB,CAgEzB;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,iBAAiB,GACzB,mBAAmB,GAAG,SAAS,CAIjC;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,iBAAiB,EAC1B,WAAW,EAAE,mBAAmB,GAC/B,IAAI,CAGN;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,SAAS,CAAC;CAC1B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB"}