@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.
- package/dist/adapters/binding.d.ts.map +1 -1
- package/dist/adapters/binding.js +37 -7
- package/dist/adapters/dto-binding-plan.d.ts +3 -0
- package/dist/adapters/dto-binding-plan.d.ts.map +1 -1
- package/dist/adapters/dto-binding-plan.js +34 -2
- package/dist/adapters/dto-validation-adapter.d.ts +1 -1
- package/dist/adapters/dto-validation-adapter.d.ts.map +1 -1
- package/dist/adapters/dto-validation-adapter.js +7 -14
- package/dist/dispatch/dispatch-handler-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-handler-policy.js +5 -3
- package/dist/dispatch/dispatch-response-policy.d.ts +1 -1
- package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-response-policy.js +3 -4
- package/dist/dispatch/dispatch-routing-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-routing-policy.js +6 -4
- package/dist/dispatch/dispatcher.d.ts +18 -6
- package/dist/dispatch/dispatcher.d.ts.map +1 -1
- package/dist/dispatch/dispatcher.js +317 -52
- package/dist/dispatch/fast-path/debug-visibility.d.ts +18 -0
- package/dist/dispatch/fast-path/debug-visibility.d.ts.map +1 -0
- package/dist/dispatch/fast-path/debug-visibility.js +39 -0
- package/dist/dispatch/fast-path/eligibility-checker.d.ts +22 -0
- package/dist/dispatch/fast-path/eligibility-checker.d.ts.map +1 -0
- package/dist/dispatch/fast-path/eligibility-checker.js +110 -0
- package/dist/dispatch/fast-path/eligibility.d.ts +61 -0
- package/dist/dispatch/fast-path/eligibility.d.ts.map +1 -0
- package/dist/dispatch/fast-path/eligibility.js +23 -0
- package/dist/dispatch/fast-path/fast-path-executor.d.ts +21 -0
- package/dist/dispatch/fast-path/fast-path-executor.d.ts.map +1 -0
- package/dist/dispatch/fast-path/fast-path-executor.js +80 -0
- package/dist/dispatch/fast-path/index.d.ts +6 -0
- package/dist/dispatch/fast-path/index.d.ts.map +1 -0
- package/dist/dispatch/fast-path/index.js +4 -0
- package/dist/dispatch/native-route-handoff.d.ts.map +1 -1
- package/dist/dispatch/native-route-handoff.js +8 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- 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 {
|
|
11
|
-
import {
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
148
|
-
return
|
|
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
|
|
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,
|
|
201
|
-
const routeGuards =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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"}
|