@fluojs/http 1.0.0-beta.1 → 1.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +36 -5
- package/README.md +36 -5
- package/dist/adapter.d.ts +31 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +37 -0
- package/dist/adapters/binding.d.ts +6 -0
- package/dist/adapters/binding.d.ts.map +1 -1
- package/dist/adapters/binding.js +54 -55
- package/dist/adapters/dto-binding-plan.d.ts +22 -0
- package/dist/adapters/dto-binding-plan.d.ts.map +1 -0
- package/dist/adapters/dto-binding-plan.js +86 -0
- package/dist/adapters/dto-validation-adapter.d.ts +3 -1
- package/dist/adapters/dto-validation-adapter.d.ts.map +1 -1
- package/dist/adapters/dto-validation-adapter.js +10 -16
- package/dist/context/sse.d.ts +38 -0
- package/dist/context/sse.d.ts.map +1 -1
- package/dist/context/sse.js +50 -2
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +262 -53
- package/dist/dispatch/dispatch-content-negotiation.d.ts +17 -0
- package/dist/dispatch/dispatch-content-negotiation.d.ts.map +1 -1
- package/dist/dispatch/dispatch-content-negotiation.js +21 -0
- package/dist/dispatch/dispatch-error-policy.d.ts +8 -0
- package/dist/dispatch/dispatch-error-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-error-policy.js +9 -0
- package/dist/dispatch/dispatch-handler-policy.d.ts +11 -1
- package/dist/dispatch/dispatch-handler-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-handler-policy.js +17 -5
- package/dist/dispatch/dispatch-response-policy.d.ts +11 -1
- package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-response-policy.js +44 -2
- package/dist/dispatch/dispatch-routing-policy.d.ts +13 -0
- package/dist/dispatch/dispatch-routing-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-routing-policy.js +49 -4
- package/dist/dispatch/dispatcher.d.ts +24 -7
- package/dist/dispatch/dispatcher.d.ts.map +1 -1
- package/dist/dispatch/dispatcher.js +460 -48
- 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 +107 -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 +53 -0
- package/dist/dispatch/native-route-handoff.d.ts.map +1 -0
- package/dist/dispatch/native-route-handoff.js +97 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/guards.d.ts +7 -0
- package/dist/guards.d.ts.map +1 -1
- package/dist/guards.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/input-error-detail.d.ts +9 -0
- package/dist/input-error-detail.d.ts.map +1 -1
- package/dist/input-error-detail.js +10 -0
- package/dist/interceptors.d.ts +8 -0
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js +14 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -1
- package/dist/mapping.d.ts +7 -0
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +93 -11
- package/dist/middleware/correlation.d.ts +5 -0
- package/dist/middleware/correlation.d.ts.map +1 -1
- package/dist/middleware/correlation.js +6 -0
- package/dist/middleware/cors.d.ts +9 -0
- package/dist/middleware/cors.d.ts.map +1 -1
- package/dist/middleware/cors.js +11 -0
- package/dist/middleware/middleware.d.ts +34 -0
- package/dist/middleware/middleware.d.ts.map +1 -1
- package/dist/middleware/middleware.js +47 -0
- package/dist/middleware/security-headers.d.ts +9 -0
- package/dist/middleware/security-headers.d.ts.map +1 -1
- package/dist/middleware/security-headers.js +11 -0
- package/dist/route-path.d.ts +41 -0
- package/dist/route-path.d.ts.map +1 -1
- package/dist/route-path.js +50 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 {
|
|
9
|
-
import {
|
|
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
|
-
|
|
28
|
-
|
|
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,
|
|
41
|
-
|
|
42
|
-
container
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
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,68 @@ 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
|
+
const dispatchRequest = request;
|
|
360
|
+
const dispatchScope = createRootDispatchScope(options.rootContainer);
|
|
361
|
+
let phaseContext;
|
|
362
|
+
let containerPromotionOpen = true;
|
|
363
|
+
const requestContext = createDispatchContext(dispatchRequest, response, dispatchScope.container, () => {
|
|
364
|
+
if (!containerPromotionOpen) {
|
|
365
|
+
return phaseContext.dispatchScope.container;
|
|
366
|
+
}
|
|
367
|
+
ensureRequestScope(phaseContext);
|
|
368
|
+
return phaseContext.dispatchScope.container;
|
|
369
|
+
});
|
|
370
|
+
phaseContext = {
|
|
371
|
+
contentNegotiation,
|
|
372
|
+
dispatchScope,
|
|
373
|
+
fastPathRuntimeCache,
|
|
374
|
+
handlerExecutionPlans: EMPTY_NATIVE_FAST_PATH_HANDLER_EXECUTION_PLANS,
|
|
375
|
+
observers: EMPTY_NATIVE_FAST_PATH_OBSERVERS,
|
|
376
|
+
options,
|
|
377
|
+
requestContext,
|
|
378
|
+
response
|
|
379
|
+
};
|
|
380
|
+
phaseContext.matchedHandler = match.descriptor;
|
|
381
|
+
updateRequestParams(phaseContext.requestContext, match.params);
|
|
382
|
+
await runWithRequestContext(phaseContext.requestContext, async () => {
|
|
383
|
+
try {
|
|
384
|
+
ensureRequestNotAborted(phaseContext.requestContext.request);
|
|
385
|
+
const fastPathSuccess = await tryFastPathExecution(match.descriptor, phaseContext);
|
|
386
|
+
if (!fastPathSuccess) {
|
|
387
|
+
throw new Error(`Native route ${match.descriptor.route.method}:${match.descriptor.route.path} was not fast-path executable.`);
|
|
388
|
+
}
|
|
389
|
+
} catch (error) {
|
|
390
|
+
await handleDispatchError(phaseContext, error);
|
|
391
|
+
} finally {
|
|
392
|
+
if (!phaseContext.dispatchScope.requestScoped) {
|
|
393
|
+
phaseContext.requestContext.container = phaseContext.dispatchScope.container;
|
|
394
|
+
}
|
|
395
|
+
containerPromotionOpen = false;
|
|
396
|
+
if (phaseContext.dispatchScope.requestScoped) {
|
|
397
|
+
try {
|
|
398
|
+
await phaseContext.dispatchScope.container.dispose();
|
|
399
|
+
} catch (error) {
|
|
400
|
+
logDispatchFailure(options.logger, 'Request-scoped container dispose threw an error.', error);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
105
407
|
async function notifyRequestStart(context) {
|
|
106
408
|
await notifyObserversSafely(context.observers, context.requestContext, async (observer, observationContext) => {
|
|
107
409
|
await observer.onRequestStart?.(observationContext);
|
|
@@ -122,6 +424,36 @@ async function notifyRequestFinish(context) {
|
|
|
122
424
|
await observer.onRequestFinish?.(observationContext);
|
|
123
425
|
}, context.options.logger, context.matchedHandler);
|
|
124
426
|
}
|
|
427
|
+
async function tryFastPathExecution(handler, context) {
|
|
428
|
+
const eligibility = getHandlerFastPathEligibility(handler);
|
|
429
|
+
if (!eligibility || eligibility.executionPath !== 'fast') {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
if (typeof context.dispatchScope.container.resolve !== 'function') {
|
|
433
|
+
ensureRequestScope(context);
|
|
434
|
+
}
|
|
435
|
+
const runtimeCache = resolveFastPathHandlerRuntimeCache(handler, context.fastPathRuntimeCache);
|
|
436
|
+
const controllerOrPromise = resolveFastPathController(handler, context.dispatchScope.container, runtimeCache);
|
|
437
|
+
const controller = isPromiseLike(controllerOrPromise) ? await controllerOrPromise : controllerOrPromise;
|
|
438
|
+
const fastPathResult = await executeFastPath({
|
|
439
|
+
binder: context.options.binder,
|
|
440
|
+
contentNegotiation: context.contentNegotiation,
|
|
441
|
+
controller,
|
|
442
|
+
controllerContainer: context.dispatchScope.container,
|
|
443
|
+
handler,
|
|
444
|
+
method: runtimeCache.method,
|
|
445
|
+
request: context.requestContext.request,
|
|
446
|
+
requestContext: context.requestContext,
|
|
447
|
+
response: context.response
|
|
448
|
+
});
|
|
449
|
+
if (fastPathResult.executed) {
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
if (fastPathResult.error) {
|
|
453
|
+
throw fastPathResult.error;
|
|
454
|
+
}
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
125
457
|
async function runDispatchPipeline(context) {
|
|
126
458
|
ensureRequestNotAborted(context.requestContext.request);
|
|
127
459
|
const appMiddlewareContext = {
|
|
@@ -129,13 +461,28 @@ async function runDispatchPipeline(context) {
|
|
|
129
461
|
requestContext: context.requestContext,
|
|
130
462
|
response: context.response
|
|
131
463
|
};
|
|
132
|
-
|
|
464
|
+
const dispatchMatchedRoute = async () => {
|
|
133
465
|
if (context.response.committed) {
|
|
134
466
|
return;
|
|
135
467
|
}
|
|
136
|
-
const match = matchHandlerOrThrow(context.options.handlerMapping, appMiddlewareContext.request);
|
|
468
|
+
const match = readFrameworkRequestNativeRouteHandoff(appMiddlewareContext.request) ?? matchHandlerOrThrow(context.options.handlerMapping, appMiddlewareContext.request);
|
|
137
469
|
context.matchedHandler = match.descriptor;
|
|
138
470
|
updateRequestParams(context.requestContext, match.params);
|
|
471
|
+
const eligibility = getHandlerFastPathEligibility(match.descriptor);
|
|
472
|
+
if (context.options.fastPathDebugHeaders === true && eligibility && !context.response.committed) {
|
|
473
|
+
const debugInfo = createPathDebugInfo(eligibility);
|
|
474
|
+
addPathDebugHeader(context.response.setHeader.bind(context.response), debugInfo);
|
|
475
|
+
}
|
|
476
|
+
if (shouldUseFastPathForRequest(eligibility, appMiddlewareContext.request)) {
|
|
477
|
+
const fastPathSuccess = await tryFastPathExecution(match.descriptor, context);
|
|
478
|
+
if (fastPathSuccess) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const executionPlan = resolveHandlerExecutionPlan(match.descriptor, context.handlerExecutionPlans, context.options);
|
|
483
|
+
if (handlerMayRequireRequestScope(executionPlan, appMiddlewareContext.request)) {
|
|
484
|
+
ensureRequestScope(context);
|
|
485
|
+
}
|
|
139
486
|
await notifyHandlerMatched(context, match.descriptor);
|
|
140
487
|
const moduleMiddlewareContext = {
|
|
141
488
|
request: context.requestContext.request,
|
|
@@ -143,12 +490,18 @@ async function runDispatchPipeline(context) {
|
|
|
143
490
|
response: context.response
|
|
144
491
|
};
|
|
145
492
|
await runMiddlewareChain(match.descriptor.metadata.moduleMiddleware ?? [], moduleMiddlewareContext, async () => {
|
|
146
|
-
await dispatchMatchedHandler(match.descriptor, context.requestContext, context.
|
|
493
|
+
await dispatchMatchedHandler(match.descriptor, executionPlan, context.requestContext, context.dispatchScope.container, context.observers, context.contentNegotiation, context.options.binder, context.options.logger);
|
|
147
494
|
});
|
|
148
|
-
}
|
|
495
|
+
};
|
|
496
|
+
const appMiddleware = context.options.appMiddleware ?? [];
|
|
497
|
+
if (appMiddleware.length === 0) {
|
|
498
|
+
await dispatchMatchedRoute();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
await runMiddlewareChain(appMiddleware, appMiddlewareContext, dispatchMatchedRoute);
|
|
149
502
|
}
|
|
150
503
|
async function handleDispatchError(context, error) {
|
|
151
|
-
if (error instanceof RequestAbortedError || context.requestContext.request
|
|
504
|
+
if (error instanceof RequestAbortedError || isRequestAborted(context.requestContext.request)) {
|
|
152
505
|
return;
|
|
153
506
|
}
|
|
154
507
|
await notifyRequestError(context, error);
|
|
@@ -167,30 +520,89 @@ async function handleDispatchError(context, error) {
|
|
|
167
520
|
*/
|
|
168
521
|
export function createDispatcher(options) {
|
|
169
522
|
const contentNegotiation = resolveContentNegotiation(options.contentNegotiation);
|
|
170
|
-
|
|
523
|
+
const observers = options.observers ?? [];
|
|
524
|
+
const appMiddleware = options.appMiddleware ?? [];
|
|
525
|
+
const dispatchStartPlan = compileDispatchStartPlan(observers, appMiddleware);
|
|
526
|
+
const fastPathRuntimeCache = new WeakMap();
|
|
527
|
+
const handlerExecutionPlans = new WeakMap();
|
|
528
|
+
const adapter = options.adapter ?? 'default';
|
|
529
|
+
const fastPathEligibilities = [];
|
|
530
|
+
for (const descriptor of options.handlerMapping.descriptors) {
|
|
531
|
+
handlerExecutionPlans.set(descriptor, compileHandlerExecutionPlan(descriptor, options));
|
|
532
|
+
const {
|
|
533
|
+
eligibility
|
|
534
|
+
} = compileFastPathEligibility(descriptor, options, adapter);
|
|
535
|
+
setHandlerFastPathEligibility(descriptor, eligibility);
|
|
536
|
+
fastPathEligibilities.push(eligibility);
|
|
537
|
+
}
|
|
538
|
+
const fastPathStats = createFastPathStats(fastPathEligibilities);
|
|
539
|
+
const dispatcher = {
|
|
540
|
+
describeRoutes() {
|
|
541
|
+
return options.handlerMapping.descriptors.map(descriptor => cloneHandlerDescriptor(descriptor));
|
|
542
|
+
},
|
|
543
|
+
async dispatchNativeRoute(match, request, response) {
|
|
544
|
+
return dispatchNativeFastRoute(match, request, response, options, contentNegotiation, fastPathRuntimeCache);
|
|
545
|
+
},
|
|
171
546
|
async dispatch(request, response) {
|
|
172
|
-
const
|
|
547
|
+
const dispatchRequest = createDispatchRequest(request);
|
|
548
|
+
const dispatchScope = dispatchStartMayRequireRequestScope(dispatchStartPlan, dispatchRequest) ? createRequestDispatchScope(options.rootContainer) : createRootDispatchScope(options.rootContainer);
|
|
549
|
+
let phaseContext;
|
|
550
|
+
let containerPromotionOpen = true;
|
|
551
|
+
const requestContext = createDispatchContext(dispatchRequest, response, dispatchScope.container, () => {
|
|
552
|
+
if (!containerPromotionOpen) {
|
|
553
|
+
return phaseContext.dispatchScope.container;
|
|
554
|
+
}
|
|
555
|
+
ensureRequestScope(phaseContext);
|
|
556
|
+
return phaseContext.dispatchScope.container;
|
|
557
|
+
});
|
|
558
|
+
phaseContext = {
|
|
173
559
|
contentNegotiation,
|
|
174
|
-
|
|
560
|
+
dispatchScope,
|
|
561
|
+
fastPathRuntimeCache,
|
|
562
|
+
handlerExecutionPlans,
|
|
563
|
+
observers,
|
|
175
564
|
options,
|
|
176
|
-
requestContext
|
|
565
|
+
requestContext,
|
|
177
566
|
response
|
|
178
567
|
};
|
|
179
568
|
await runWithRequestContext(phaseContext.requestContext, async () => {
|
|
180
569
|
try {
|
|
181
|
-
|
|
570
|
+
if (observers.length > 0) {
|
|
571
|
+
await notifyRequestStart(phaseContext);
|
|
572
|
+
}
|
|
182
573
|
await runDispatchPipeline(phaseContext);
|
|
183
574
|
} catch (error) {
|
|
184
575
|
await handleDispatchError(phaseContext, error);
|
|
185
576
|
} finally {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
577
|
+
if (observers.length > 0) {
|
|
578
|
+
await notifyRequestFinish(phaseContext);
|
|
579
|
+
}
|
|
580
|
+
if (!phaseContext.dispatchScope.requestScoped) {
|
|
581
|
+
phaseContext.requestContext.container = phaseContext.dispatchScope.container;
|
|
582
|
+
}
|
|
583
|
+
containerPromotionOpen = false;
|
|
584
|
+
if (phaseContext.dispatchScope.requestScoped) {
|
|
585
|
+
try {
|
|
586
|
+
await phaseContext.dispatchScope.container.dispose();
|
|
587
|
+
} catch (error) {
|
|
588
|
+
logDispatchFailure(options.logger, 'Request-scoped container dispose threw an error.', error);
|
|
589
|
+
}
|
|
191
590
|
}
|
|
192
591
|
}
|
|
193
592
|
});
|
|
194
593
|
}
|
|
195
594
|
};
|
|
196
|
-
|
|
595
|
+
dispatcher[FAST_PATH_STATS_SYMBOL] = fastPathStats;
|
|
596
|
+
return dispatcher;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Reads automatic fast-path eligibility statistics attached to a dispatcher.
|
|
601
|
+
*
|
|
602
|
+
* @param dispatcher Dispatcher returned by {@link createDispatcher}.
|
|
603
|
+
* @returns Fast-path statistics when available.
|
|
604
|
+
*/
|
|
605
|
+
export function getDispatcherFastPathStats(dispatcher) {
|
|
606
|
+
return dispatcher[FAST_PATH_STATS_SYMBOL];
|
|
607
|
+
}
|
|
608
|
+
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
|
+
}
|