@fluojs/http 1.0.0-beta.1 → 1.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +39 -5
- package/README.md +39 -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/request-context-node-store.d.ts +25 -0
- package/dist/context/request-context-node-store.d.ts.map +1 -0
- package/dist/context/request-context-node-store.js +32 -0
- package/dist/context/request-context-stack-store.d.ts +8 -0
- package/dist/context/request-context-stack-store.d.ts.map +1 -0
- package/dist/context/request-context-stack-store.js +29 -0
- package/dist/context/request-context-store.d.ts +7 -0
- package/dist/context/request-context-store.d.ts.map +1 -0
- package/dist/context/request-context-store.js +1 -0
- package/dist/context/request-context.d.ts +7 -3
- package/dist/context/request-context.d.ts.map +1 -1
- package/dist/context/request-context.js +17 -5
- 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 +464 -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 +3 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -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 +14 -2
- 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,72 @@ async function dispatchMatchedHandler(handler, requestContext, observers, conten
|
|
|
102
342
|
await observer.onRequestSuccess?.(context, result);
|
|
103
343
|
}, logger, handler);
|
|
104
344
|
}
|
|
345
|
+
function resolveHandlerExecutionPlan(handler, executionPlans, options) {
|
|
346
|
+
const cached = executionPlans.get(handler);
|
|
347
|
+
if (cached) {
|
|
348
|
+
return cached;
|
|
349
|
+
}
|
|
350
|
+
const compiled = compileHandlerExecutionPlan(handler, options);
|
|
351
|
+
executionPlans.set(handler, compiled);
|
|
352
|
+
return compiled;
|
|
353
|
+
}
|
|
354
|
+
async function dispatchNativeFastRoute(match, request, response, options, contentNegotiation, fastPathRuntimeCache) {
|
|
355
|
+
const eligibility = getHandlerFastPathEligibility(match.descriptor);
|
|
356
|
+
if (!shouldUseFastPathForRequest(eligibility, request)) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
if (options.fastPathDebugHeaders === true && eligibility && !response.committed) {
|
|
360
|
+
const debugInfo = createPathDebugInfo(eligibility);
|
|
361
|
+
addPathDebugHeader(response.setHeader.bind(response), debugInfo);
|
|
362
|
+
}
|
|
363
|
+
const dispatchRequest = request;
|
|
364
|
+
const dispatchScope = createRootDispatchScope(options.rootContainer);
|
|
365
|
+
let phaseContext;
|
|
366
|
+
let containerPromotionOpen = true;
|
|
367
|
+
const requestContext = createDispatchContext(dispatchRequest, response, dispatchScope.container, () => {
|
|
368
|
+
if (!containerPromotionOpen) {
|
|
369
|
+
return phaseContext.dispatchScope.container;
|
|
370
|
+
}
|
|
371
|
+
ensureRequestScope(phaseContext);
|
|
372
|
+
return phaseContext.dispatchScope.container;
|
|
373
|
+
});
|
|
374
|
+
phaseContext = {
|
|
375
|
+
contentNegotiation,
|
|
376
|
+
dispatchScope,
|
|
377
|
+
fastPathRuntimeCache,
|
|
378
|
+
handlerExecutionPlans: EMPTY_NATIVE_FAST_PATH_HANDLER_EXECUTION_PLANS,
|
|
379
|
+
observers: EMPTY_NATIVE_FAST_PATH_OBSERVERS,
|
|
380
|
+
options,
|
|
381
|
+
requestContext,
|
|
382
|
+
response
|
|
383
|
+
};
|
|
384
|
+
phaseContext.matchedHandler = match.descriptor;
|
|
385
|
+
updateRequestParams(phaseContext.requestContext, match.params);
|
|
386
|
+
await runWithRequestContext(phaseContext.requestContext, async () => {
|
|
387
|
+
try {
|
|
388
|
+
ensureRequestNotAborted(phaseContext.requestContext.request);
|
|
389
|
+
const fastPathSuccess = await tryFastPathExecution(match.descriptor, phaseContext);
|
|
390
|
+
if (!fastPathSuccess) {
|
|
391
|
+
throw new Error(`Native route ${match.descriptor.route.method}:${match.descriptor.route.path} was not fast-path executable.`);
|
|
392
|
+
}
|
|
393
|
+
} catch (error) {
|
|
394
|
+
await handleDispatchError(phaseContext, error);
|
|
395
|
+
} finally {
|
|
396
|
+
if (!phaseContext.dispatchScope.requestScoped) {
|
|
397
|
+
phaseContext.requestContext.container = phaseContext.dispatchScope.container;
|
|
398
|
+
}
|
|
399
|
+
containerPromotionOpen = false;
|
|
400
|
+
if (phaseContext.dispatchScope.requestScoped) {
|
|
401
|
+
try {
|
|
402
|
+
await phaseContext.dispatchScope.container.dispose();
|
|
403
|
+
} catch (error) {
|
|
404
|
+
logDispatchFailure(options.logger, 'Request-scoped container dispose threw an error.', error);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
105
411
|
async function notifyRequestStart(context) {
|
|
106
412
|
await notifyObserversSafely(context.observers, context.requestContext, async (observer, observationContext) => {
|
|
107
413
|
await observer.onRequestStart?.(observationContext);
|
|
@@ -122,6 +428,36 @@ async function notifyRequestFinish(context) {
|
|
|
122
428
|
await observer.onRequestFinish?.(observationContext);
|
|
123
429
|
}, context.options.logger, context.matchedHandler);
|
|
124
430
|
}
|
|
431
|
+
async function tryFastPathExecution(handler, context) {
|
|
432
|
+
const eligibility = getHandlerFastPathEligibility(handler);
|
|
433
|
+
if (!eligibility || eligibility.executionPath !== 'fast') {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
if (typeof context.dispatchScope.container.resolve !== 'function') {
|
|
437
|
+
ensureRequestScope(context);
|
|
438
|
+
}
|
|
439
|
+
const runtimeCache = resolveFastPathHandlerRuntimeCache(handler, context.fastPathRuntimeCache);
|
|
440
|
+
const controllerOrPromise = resolveFastPathController(handler, context.dispatchScope.container, runtimeCache);
|
|
441
|
+
const controller = isPromiseLike(controllerOrPromise) ? await controllerOrPromise : controllerOrPromise;
|
|
442
|
+
const fastPathResult = await executeFastPath({
|
|
443
|
+
binder: context.options.binder,
|
|
444
|
+
contentNegotiation: context.contentNegotiation,
|
|
445
|
+
controller,
|
|
446
|
+
controllerContainer: context.dispatchScope.container,
|
|
447
|
+
handler,
|
|
448
|
+
method: runtimeCache.method,
|
|
449
|
+
request: context.requestContext.request,
|
|
450
|
+
requestContext: context.requestContext,
|
|
451
|
+
response: context.response
|
|
452
|
+
});
|
|
453
|
+
if (fastPathResult.executed) {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
if (fastPathResult.error) {
|
|
457
|
+
throw fastPathResult.error;
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
125
461
|
async function runDispatchPipeline(context) {
|
|
126
462
|
ensureRequestNotAborted(context.requestContext.request);
|
|
127
463
|
const appMiddlewareContext = {
|
|
@@ -129,13 +465,28 @@ async function runDispatchPipeline(context) {
|
|
|
129
465
|
requestContext: context.requestContext,
|
|
130
466
|
response: context.response
|
|
131
467
|
};
|
|
132
|
-
|
|
468
|
+
const dispatchMatchedRoute = async () => {
|
|
133
469
|
if (context.response.committed) {
|
|
134
470
|
return;
|
|
135
471
|
}
|
|
136
|
-
const match = matchHandlerOrThrow(context.options.handlerMapping, appMiddlewareContext.request);
|
|
472
|
+
const match = readFrameworkRequestNativeRouteHandoff(appMiddlewareContext.request) ?? matchHandlerOrThrow(context.options.handlerMapping, appMiddlewareContext.request);
|
|
137
473
|
context.matchedHandler = match.descriptor;
|
|
138
474
|
updateRequestParams(context.requestContext, match.params);
|
|
475
|
+
const eligibility = getHandlerFastPathEligibility(match.descriptor);
|
|
476
|
+
if (context.options.fastPathDebugHeaders === true && eligibility && !context.response.committed) {
|
|
477
|
+
const debugInfo = createPathDebugInfo(eligibility);
|
|
478
|
+
addPathDebugHeader(context.response.setHeader.bind(context.response), debugInfo);
|
|
479
|
+
}
|
|
480
|
+
if (shouldUseFastPathForRequest(eligibility, appMiddlewareContext.request)) {
|
|
481
|
+
const fastPathSuccess = await tryFastPathExecution(match.descriptor, context);
|
|
482
|
+
if (fastPathSuccess) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const executionPlan = resolveHandlerExecutionPlan(match.descriptor, context.handlerExecutionPlans, context.options);
|
|
487
|
+
if (handlerMayRequireRequestScope(executionPlan, appMiddlewareContext.request)) {
|
|
488
|
+
ensureRequestScope(context);
|
|
489
|
+
}
|
|
139
490
|
await notifyHandlerMatched(context, match.descriptor);
|
|
140
491
|
const moduleMiddlewareContext = {
|
|
141
492
|
request: context.requestContext.request,
|
|
@@ -143,12 +494,18 @@ async function runDispatchPipeline(context) {
|
|
|
143
494
|
response: context.response
|
|
144
495
|
};
|
|
145
496
|
await runMiddlewareChain(match.descriptor.metadata.moduleMiddleware ?? [], moduleMiddlewareContext, async () => {
|
|
146
|
-
await dispatchMatchedHandler(match.descriptor, context.requestContext, context.
|
|
497
|
+
await dispatchMatchedHandler(match.descriptor, executionPlan, context.requestContext, context.dispatchScope.container, context.observers, context.contentNegotiation, context.options.binder, context.options.logger);
|
|
147
498
|
});
|
|
148
|
-
}
|
|
499
|
+
};
|
|
500
|
+
const appMiddleware = context.options.appMiddleware ?? [];
|
|
501
|
+
if (appMiddleware.length === 0) {
|
|
502
|
+
await dispatchMatchedRoute();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
await runMiddlewareChain(appMiddleware, appMiddlewareContext, dispatchMatchedRoute);
|
|
149
506
|
}
|
|
150
507
|
async function handleDispatchError(context, error) {
|
|
151
|
-
if (error instanceof RequestAbortedError || context.requestContext.request
|
|
508
|
+
if (error instanceof RequestAbortedError || isRequestAborted(context.requestContext.request)) {
|
|
152
509
|
return;
|
|
153
510
|
}
|
|
154
511
|
await notifyRequestError(context, error);
|
|
@@ -167,30 +524,89 @@ async function handleDispatchError(context, error) {
|
|
|
167
524
|
*/
|
|
168
525
|
export function createDispatcher(options) {
|
|
169
526
|
const contentNegotiation = resolveContentNegotiation(options.contentNegotiation);
|
|
170
|
-
|
|
527
|
+
const observers = options.observers ?? [];
|
|
528
|
+
const appMiddleware = options.appMiddleware ?? [];
|
|
529
|
+
const dispatchStartPlan = compileDispatchStartPlan(observers, appMiddleware);
|
|
530
|
+
const fastPathRuntimeCache = new WeakMap();
|
|
531
|
+
const handlerExecutionPlans = new WeakMap();
|
|
532
|
+
const adapter = options.adapter ?? 'default';
|
|
533
|
+
const fastPathEligibilities = [];
|
|
534
|
+
for (const descriptor of options.handlerMapping.descriptors) {
|
|
535
|
+
handlerExecutionPlans.set(descriptor, compileHandlerExecutionPlan(descriptor, options));
|
|
536
|
+
const {
|
|
537
|
+
eligibility
|
|
538
|
+
} = compileFastPathEligibility(descriptor, options, adapter);
|
|
539
|
+
setHandlerFastPathEligibility(descriptor, eligibility);
|
|
540
|
+
fastPathEligibilities.push(eligibility);
|
|
541
|
+
}
|
|
542
|
+
const fastPathStats = createFastPathStats(fastPathEligibilities);
|
|
543
|
+
const dispatcher = {
|
|
544
|
+
describeRoutes() {
|
|
545
|
+
return options.handlerMapping.descriptors.map(descriptor => cloneHandlerDescriptor(descriptor));
|
|
546
|
+
},
|
|
547
|
+
async dispatchNativeRoute(match, request, response) {
|
|
548
|
+
return dispatchNativeFastRoute(match, request, response, options, contentNegotiation, fastPathRuntimeCache);
|
|
549
|
+
},
|
|
171
550
|
async dispatch(request, response) {
|
|
172
|
-
const
|
|
551
|
+
const dispatchRequest = createDispatchRequest(request);
|
|
552
|
+
const dispatchScope = dispatchStartMayRequireRequestScope(dispatchStartPlan, dispatchRequest) ? createRequestDispatchScope(options.rootContainer) : createRootDispatchScope(options.rootContainer);
|
|
553
|
+
let phaseContext;
|
|
554
|
+
let containerPromotionOpen = true;
|
|
555
|
+
const requestContext = createDispatchContext(dispatchRequest, response, dispatchScope.container, () => {
|
|
556
|
+
if (!containerPromotionOpen) {
|
|
557
|
+
return phaseContext.dispatchScope.container;
|
|
558
|
+
}
|
|
559
|
+
ensureRequestScope(phaseContext);
|
|
560
|
+
return phaseContext.dispatchScope.container;
|
|
561
|
+
});
|
|
562
|
+
phaseContext = {
|
|
173
563
|
contentNegotiation,
|
|
174
|
-
|
|
564
|
+
dispatchScope,
|
|
565
|
+
fastPathRuntimeCache,
|
|
566
|
+
handlerExecutionPlans,
|
|
567
|
+
observers,
|
|
175
568
|
options,
|
|
176
|
-
requestContext
|
|
569
|
+
requestContext,
|
|
177
570
|
response
|
|
178
571
|
};
|
|
179
572
|
await runWithRequestContext(phaseContext.requestContext, async () => {
|
|
180
573
|
try {
|
|
181
|
-
|
|
574
|
+
if (observers.length > 0) {
|
|
575
|
+
await notifyRequestStart(phaseContext);
|
|
576
|
+
}
|
|
182
577
|
await runDispatchPipeline(phaseContext);
|
|
183
578
|
} catch (error) {
|
|
184
579
|
await handleDispatchError(phaseContext, error);
|
|
185
580
|
} finally {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
581
|
+
if (observers.length > 0) {
|
|
582
|
+
await notifyRequestFinish(phaseContext);
|
|
583
|
+
}
|
|
584
|
+
if (!phaseContext.dispatchScope.requestScoped) {
|
|
585
|
+
phaseContext.requestContext.container = phaseContext.dispatchScope.container;
|
|
586
|
+
}
|
|
587
|
+
containerPromotionOpen = false;
|
|
588
|
+
if (phaseContext.dispatchScope.requestScoped) {
|
|
589
|
+
try {
|
|
590
|
+
await phaseContext.dispatchScope.container.dispose();
|
|
591
|
+
} catch (error) {
|
|
592
|
+
logDispatchFailure(options.logger, 'Request-scoped container dispose threw an error.', error);
|
|
593
|
+
}
|
|
191
594
|
}
|
|
192
595
|
}
|
|
193
596
|
});
|
|
194
597
|
}
|
|
195
598
|
};
|
|
196
|
-
|
|
599
|
+
dispatcher[FAST_PATH_STATS_SYMBOL] = fastPathStats;
|
|
600
|
+
return dispatcher;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Reads automatic fast-path eligibility statistics attached to a dispatcher.
|
|
605
|
+
*
|
|
606
|
+
* @param dispatcher Dispatcher returned by {@link createDispatcher}.
|
|
607
|
+
* @returns Fast-path statistics when available.
|
|
608
|
+
*/
|
|
609
|
+
export function getDispatcherFastPathStats(dispatcher) {
|
|
610
|
+
return dispatcher[FAST_PATH_STATS_SYMBOL];
|
|
611
|
+
}
|
|
612
|
+
export { formatFastPathStats } from './fast-path/index.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FastPathEligibility, FastPathStats } from './eligibility.js';
|
|
2
|
+
interface PathDebugInfo {
|
|
3
|
+
executionPath: 'fast' | 'full';
|
|
4
|
+
fallbackReason?: string;
|
|
5
|
+
routeId: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createPathDebugInfo(eligibility: FastPathEligibility): PathDebugInfo;
|
|
8
|
+
export declare function addPathDebugHeader(setHeader: (name: string, value: string) => void, info: PathDebugInfo): void;
|
|
9
|
+
export declare function createFastPathStats(eligibilities: readonly FastPathEligibility[]): FastPathStats;
|
|
10
|
+
/**
|
|
11
|
+
* Formats dispatcher fast-path statistics for debug logs and benchmark output.
|
|
12
|
+
*
|
|
13
|
+
* @param stats Fast-path statistics returned by {@link getDispatcherFastPathStats}.
|
|
14
|
+
* @returns A human-readable route breakdown.
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatFastPathStats(stats: FastPathStats): string;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=debug-visibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debug-visibility.d.ts","sourceRoot":"","sources":["../../../src/dispatch/fast-path/debug-visibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAI3E,UAAU,aAAa;IACrB,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,mBAAmB,GAAG,aAAa,CAMnF;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,EAChD,IAAI,EAAE,aAAa,GAClB,IAAI,CAMN;AAED,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,SAAS,mBAAmB,EAAE,GAAG,aAAa,CAShG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAuBhE"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const DEBUG_HEADER_NAME = 'X-Fluo-Path';
|
|
2
|
+
export function createPathDebugInfo(eligibility) {
|
|
3
|
+
return {
|
|
4
|
+
executionPath: eligibility.executionPath,
|
|
5
|
+
fallbackReason: eligibility.fallbackReason,
|
|
6
|
+
routeId: eligibility.routeId
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function addPathDebugHeader(setHeader, info) {
|
|
10
|
+
const value = info.executionPath === 'fast' ? `fast; route=${info.routeId}` : `full; route=${info.routeId}; reason=${info.fallbackReason ?? 'none'}`;
|
|
11
|
+
setHeader(DEBUG_HEADER_NAME, value);
|
|
12
|
+
}
|
|
13
|
+
export function createFastPathStats(eligibilities) {
|
|
14
|
+
const fastPathRoutes = eligibilities.filter(e => e.executionPath === 'fast').length;
|
|
15
|
+
return {
|
|
16
|
+
fastPathRoutes,
|
|
17
|
+
fullPathRoutes: eligibilities.length - fastPathRoutes,
|
|
18
|
+
routes: eligibilities,
|
|
19
|
+
totalRoutes: eligibilities.length
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Formats dispatcher fast-path statistics for debug logs and benchmark output.
|
|
25
|
+
*
|
|
26
|
+
* @param stats Fast-path statistics returned by {@link getDispatcherFastPathStats}.
|
|
27
|
+
* @returns A human-readable route breakdown.
|
|
28
|
+
*/
|
|
29
|
+
export function formatFastPathStats(stats) {
|
|
30
|
+
const fastPathPercent = stats.totalRoutes === 0 ? '0.0' : (stats.fastPathRoutes / stats.totalRoutes * 100).toFixed(1);
|
|
31
|
+
const fullPathPercent = stats.totalRoutes === 0 ? '0.0' : (stats.fullPathRoutes / stats.totalRoutes * 100).toFixed(1);
|
|
32
|
+
const lines = ['=== Fast Path Statistics ===', `Total routes: ${String(stats.totalRoutes)}`, `Fast path: ${String(stats.fastPathRoutes)} (${fastPathPercent}%)`, `Full path: ${String(stats.fullPathRoutes)} (${fullPathPercent}%)`, '', 'Route breakdown:'];
|
|
33
|
+
for (const route of stats.routes) {
|
|
34
|
+
const status = route.executionPath === 'fast' ? 'FAST' : 'FULL';
|
|
35
|
+
const reason = route.fallbackReason ? ` (${route.fallbackReason})` : '';
|
|
36
|
+
lines.push(` [${status}] ${route.routeId}${reason}`);
|
|
37
|
+
}
|
|
38
|
+
return lines.join('\n');
|
|
39
|
+
}
|