@nestjs/core 11.1.23 → 11.1.25
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.md +2 -0
- package/errors/exceptions/route-conflict.exception.d.ts +4 -0
- package/errors/exceptions/route-conflict.exception.js +7 -0
- package/helpers/handler-metadata-storage.d.ts +1 -0
- package/injector/instance-wrapper.d.ts +2 -0
- package/injector/instance-wrapper.js +25 -0
- package/nest-application.d.ts +1 -0
- package/nest-application.js +5 -0
- package/package.json +3 -14
- package/router/interfaces/resolved-route.interface.d.ts +32 -0
- package/router/interfaces/resolved-route.interface.js +1 -0
- package/router/interfaces/route-conflict.interface.d.ts +14 -0
- package/router/interfaces/route-conflict.interface.js +1 -0
- package/router/interfaces/route-resolution-options.interface.d.ts +24 -0
- package/router/interfaces/route-resolution-options.interface.js +1 -0
- package/router/route-conflict-detector.d.ts +71 -0
- package/router/route-conflict-detector.js +276 -0
- package/router/route-specificity-sorter.d.ts +23 -0
- package/router/route-specificity-sorter.js +59 -0
- package/router/router-execution-context.js +5 -2
- package/router/router-response-controller.js +77 -49
package/Readme.md
CHANGED
|
@@ -96,6 +96,8 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
|
|
96
96
|
</tr><tr>
|
|
97
97
|
<td align="center" valign="middle"><a href="https://pandektes.com" target="_blank"><img src="https://nestjs.com/img/logos/pandektes-logo.png" width="65" valign="middle" /></a></td>
|
|
98
98
|
<td align="center" valign="middle"><a href="https://www.fintechcrafts.com/" target="_blank"><img src="https://nestjs.com/img/logos/fintechcrafts-logo.svg" width="65" valign="middle" /></a></td>
|
|
99
|
+
<td align="center" valign="middle"><a href="https://talordata.com/?campaignid=Ya5Xpw2gmhyee6Tg&utm_source=nestjs&utm_term=nestjs" target="_blank"><img src="https://images.opencollective.com/elowen/9ccac7f/avatar/512.png" width="65" valign="middle" /></a></td>
|
|
100
|
+
<td align="center" valign="middle"><a href="https://www.rapidproxy.io/?ref=nestjs" target="_blank"><img src="https://nestjs.com/img/logos/rapid-logo.svg" width="120" valign="middle" /></a></td>
|
|
99
101
|
</tr>
|
|
100
102
|
</table>
|
|
101
103
|
|
|
@@ -10,6 +10,7 @@ export type HandlerResponseBasicFn = <TResult, TResponse>(result: TResult, res:
|
|
|
10
10
|
export type HandleSseResponseFn = <TResult extends Observable<unknown> = any, TResponse extends HeaderStream = any, TRequest extends IncomingMessage = any>(result: TResult, res: TResponse, req: TRequest) => any;
|
|
11
11
|
export interface HandlerMetadata {
|
|
12
12
|
argsLength: number;
|
|
13
|
+
isSseHandler: boolean;
|
|
13
14
|
paramtypes: any[];
|
|
14
15
|
httpStatusCode: number;
|
|
15
16
|
responseHeaders: any[];
|
|
@@ -91,6 +91,8 @@ export declare class InstanceWrapper<T = any> {
|
|
|
91
91
|
getStaticTransientInstances(): (InstancePerContext<T> | undefined)[];
|
|
92
92
|
mergeWith(provider: Provider): void;
|
|
93
93
|
private isNewable;
|
|
94
|
+
private registerDependencyTreeParent;
|
|
95
|
+
private resetDependencyTreeState;
|
|
94
96
|
private initialize;
|
|
95
97
|
private printIntrospectedAsRequestScoped;
|
|
96
98
|
private printIntrospectedAsDurable;
|
|
@@ -12,6 +12,7 @@ const constants_1 = require("./constants");
|
|
|
12
12
|
const provider_classifier_1 = require("./helpers/provider-classifier");
|
|
13
13
|
exports.INSTANCE_METADATA_SYMBOL = Symbol.for('instance_metadata:cache');
|
|
14
14
|
exports.INSTANCE_ID_SYMBOL = Symbol.for('instance_metadata:id');
|
|
15
|
+
const dependencyTreeParents = new WeakMap();
|
|
15
16
|
class InstanceWrapper {
|
|
16
17
|
constructor(metadata = {}) {
|
|
17
18
|
this.isAlias = false;
|
|
@@ -99,6 +100,8 @@ class InstanceWrapper {
|
|
|
99
100
|
this[exports.INSTANCE_METADATA_SYMBOL].dependencies = [];
|
|
100
101
|
}
|
|
101
102
|
this[exports.INSTANCE_METADATA_SYMBOL].dependencies[index] = wrapper;
|
|
103
|
+
this.registerDependencyTreeParent(wrapper);
|
|
104
|
+
this.resetDependencyTreeState();
|
|
102
105
|
}
|
|
103
106
|
getCtorMetadata() {
|
|
104
107
|
return this[exports.INSTANCE_METADATA_SYMBOL].dependencies;
|
|
@@ -111,6 +114,8 @@ class InstanceWrapper {
|
|
|
111
114
|
key,
|
|
112
115
|
wrapper,
|
|
113
116
|
});
|
|
117
|
+
this.registerDependencyTreeParent(wrapper);
|
|
118
|
+
this.resetDependencyTreeState();
|
|
114
119
|
}
|
|
115
120
|
getPropertiesMetadata() {
|
|
116
121
|
return this[exports.INSTANCE_METADATA_SYMBOL].properties;
|
|
@@ -120,6 +125,8 @@ class InstanceWrapper {
|
|
|
120
125
|
this[exports.INSTANCE_METADATA_SYMBOL].enhancers = [];
|
|
121
126
|
}
|
|
122
127
|
this[exports.INSTANCE_METADATA_SYMBOL].enhancers.push(wrapper);
|
|
128
|
+
this.registerDependencyTreeParent(wrapper);
|
|
129
|
+
this.resetDependencyTreeState();
|
|
123
130
|
}
|
|
124
131
|
getEnhancersMetadata() {
|
|
125
132
|
return this[exports.INSTANCE_METADATA_SYMBOL].enhancers;
|
|
@@ -311,6 +318,24 @@ class InstanceWrapper {
|
|
|
311
318
|
isNewable() {
|
|
312
319
|
return (0, shared_utils_1.isNil)(this.inject) && this.metatype && this.metatype.prototype;
|
|
313
320
|
}
|
|
321
|
+
registerDependencyTreeParent(wrapper) {
|
|
322
|
+
if (wrapper instanceof InstanceWrapper) {
|
|
323
|
+
const parents = dependencyTreeParents.get(wrapper) ?? new Set();
|
|
324
|
+
parents.add(this);
|
|
325
|
+
dependencyTreeParents.set(wrapper, parents);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
resetDependencyTreeState(lookupRegistry = new Set()) {
|
|
329
|
+
if (lookupRegistry.has(this[exports.INSTANCE_ID_SYMBOL])) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
lookupRegistry.add(this[exports.INSTANCE_ID_SYMBOL]);
|
|
333
|
+
this.isTreeStatic = undefined;
|
|
334
|
+
this.isTreeDurable = undefined;
|
|
335
|
+
dependencyTreeParents
|
|
336
|
+
.get(this)
|
|
337
|
+
?.forEach(parent => parent.resetDependencyTreeState(lookupRegistry));
|
|
338
|
+
}
|
|
314
339
|
initialize(metadata) {
|
|
315
340
|
const { instance, isResolved, ...wrapperPartial } = metadata;
|
|
316
341
|
Object.assign(this, wrapperPartial);
|
package/nest-application.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export declare class NestApplication extends NestApplicationContext<NestApplicat
|
|
|
22
22
|
private readonly microservices;
|
|
23
23
|
private httpServer;
|
|
24
24
|
private isListening;
|
|
25
|
+
private isWsModuleRegistered;
|
|
25
26
|
constructor(container: NestContainer, httpAdapter: HttpServer, config: ApplicationConfig, graphInspector: GraphInspector, appOptions?: NestApplicationOptions);
|
|
26
27
|
protected dispose(): Promise<void>;
|
|
27
28
|
getHttpAdapter(): AbstractHttpAdapter;
|
package/nest-application.js
CHANGED
|
@@ -35,6 +35,7 @@ class NestApplication extends nest_application_context_1.NestApplicationContext
|
|
|
35
35
|
this.socketModule = SocketModule && new SocketModule();
|
|
36
36
|
this.microservices = [];
|
|
37
37
|
this.isListening = false;
|
|
38
|
+
this.isWsModuleRegistered = false;
|
|
38
39
|
this.selectContextModule();
|
|
39
40
|
this.registerHttpServer();
|
|
40
41
|
this.injector = new injector_1.Injector({
|
|
@@ -89,6 +90,7 @@ class NestApplication extends nest_application_context_1.NestApplicationContext
|
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
92
|
this.socketModule.register(this.container, this.config, this.graphInspector, this.appOptions, this.httpServer);
|
|
93
|
+
this.isWsModuleRegistered = true;
|
|
92
94
|
}
|
|
93
95
|
async init() {
|
|
94
96
|
if (this.isInitialized) {
|
|
@@ -252,6 +254,9 @@ class NestApplication extends nest_application_context_1.NestApplicationContext
|
|
|
252
254
|
return this;
|
|
253
255
|
}
|
|
254
256
|
useWebSocketAdapter(adapter) {
|
|
257
|
+
if (this.isWsModuleRegistered) {
|
|
258
|
+
this.logger.warn('useWebSocketAdapter() was called after WebSocket gateways were already initialized. The provided adapter will be stored but will NOT be applied to existing gateways — they remain bound to the previously installed adapter. To install a custom adapter, call app.useWebSocketAdapter(...) BEFORE app.init() (or app.listen()).');
|
|
259
|
+
}
|
|
255
260
|
this.config.setIoAdapter(adapter);
|
|
256
261
|
return this;
|
|
257
262
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestjs/core",
|
|
3
|
-
"version": "11.1.
|
|
3
|
+
"version": "11.1.25",
|
|
4
4
|
"description": "Nest - modern, fast, powerful node.js web framework (@core)",
|
|
5
5
|
"author": "Kamil Mysliwiec",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,18 +20,7 @@
|
|
|
20
20
|
"publishConfig": {
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
|
-
"scripts": {
|
|
24
|
-
"postinstall": "opencollective || exit 0"
|
|
25
|
-
},
|
|
26
|
-
"collective": {
|
|
27
|
-
"type": "opencollective",
|
|
28
|
-
"url": "https://opencollective.com/nest",
|
|
29
|
-
"donation": {
|
|
30
|
-
"text": "Become a partner:"
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
23
|
"dependencies": {
|
|
34
|
-
"@nuxt/opencollective": "0.4.1",
|
|
35
24
|
"fast-safe-stringify": "2.1.1",
|
|
36
25
|
"iterare": "1.2.1",
|
|
37
26
|
"path-to-regexp": "8.4.2",
|
|
@@ -39,7 +28,7 @@
|
|
|
39
28
|
"uid": "2.0.2"
|
|
40
29
|
},
|
|
41
30
|
"devDependencies": {
|
|
42
|
-
"@nestjs/common": "11.1.
|
|
31
|
+
"@nestjs/common": "11.1.25"
|
|
43
32
|
},
|
|
44
33
|
"peerDependencies": {
|
|
45
34
|
"@nestjs/common": "^11.0.0",
|
|
@@ -60,5 +49,5 @@
|
|
|
60
49
|
"optional": true
|
|
61
50
|
}
|
|
62
51
|
},
|
|
63
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "02f804159841a2771755c382832a7938b904c420"
|
|
64
53
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { RequestMethod } from '@nestjs/common';
|
|
2
|
+
import { type VersionValue } from '@nestjs/common/internal';
|
|
3
|
+
import { InstanceWrapper } from '../../injector/instance-wrapper.js';
|
|
4
|
+
import { RouterProxyCallback } from '../router-proxy.js';
|
|
5
|
+
/**
|
|
6
|
+
* Loose callable signature shared by the various handler wrappers that
|
|
7
|
+
* are composed before adapter registration (host filter, version
|
|
8
|
+
* filter, request-scoped handler, etc.). They all accept the (req, res,
|
|
9
|
+
* next) trio but may be invoked variadically by adapter shims.
|
|
10
|
+
*/
|
|
11
|
+
export type ResolvedRouteHandler = (...args: unknown[]) => unknown;
|
|
12
|
+
/**
|
|
13
|
+
* Final route description produced during the "collect" phase of the
|
|
14
|
+
* router pipeline and consumed during the "register" phase. Holds the
|
|
15
|
+
* fully composed path, the pre-built handler chain (proxy + host filter
|
|
16
|
+
* + optional version filter), and the metadata needed to register the
|
|
17
|
+
* route on the HTTP adapter and to insert an entrypoint into the graph
|
|
18
|
+
* inspector.
|
|
19
|
+
*/
|
|
20
|
+
export interface ResolvedRoute {
|
|
21
|
+
method: RequestMethod;
|
|
22
|
+
path: string;
|
|
23
|
+
rawPath?: string;
|
|
24
|
+
host: string | RegExp | Array<string | RegExp> | undefined;
|
|
25
|
+
version: VersionValue | undefined;
|
|
26
|
+
methodVersion: VersionValue | undefined;
|
|
27
|
+
controllerVersion: VersionValue | undefined;
|
|
28
|
+
handler: ResolvedRouteHandler;
|
|
29
|
+
targetCallback: RouterProxyCallback;
|
|
30
|
+
methodName: string;
|
|
31
|
+
instanceWrapper: InstanceWrapper;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ResolvedRoute } from './resolved-route.interface.js';
|
|
2
|
+
/**
|
|
3
|
+
* Distinguishes the two flavors of route overlap.
|
|
4
|
+
* - `duplicate` — identical method + path + version + host registered twice.
|
|
5
|
+
* - `shadow` — patterns can match the same request but are not identical.
|
|
6
|
+
*/
|
|
7
|
+
export type ConflictKind = 'duplicate' | 'shadow';
|
|
8
|
+
export interface RouteConflict {
|
|
9
|
+
/** Route registered first; on order-sensitive adapters this wins. */
|
|
10
|
+
winner: ResolvedRoute;
|
|
11
|
+
/** Route registered later; on order-sensitive adapters this never matches. */
|
|
12
|
+
shadowed: ResolvedRoute;
|
|
13
|
+
kind: ConflictKind;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ResolvedRoute } from './resolved-route.interface.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options that control how `Resolver.resolve` walks the controller
|
|
4
|
+
* graph and registers routes on the HTTP adapter. Used internally to
|
|
5
|
+
* thread route-collection and deferred-registration concerns through
|
|
6
|
+
* the resolver chain without bloating individual method signatures.
|
|
7
|
+
*/
|
|
8
|
+
export interface RouteResolutionOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Invoked once for each route after its final path, host and version
|
|
11
|
+
* have been composed. Lets the caller observe resolved routes (for
|
|
12
|
+
* conflict detection, specificity sorting, etc.) without coupling
|
|
13
|
+
* those concerns to the resolver itself.
|
|
14
|
+
*/
|
|
15
|
+
onRouteResolved?: (route: ResolvedRoute) => void;
|
|
16
|
+
/**
|
|
17
|
+
* When `true`, the resolver still walks every controller and emits
|
|
18
|
+
* `onRouteResolved` callbacks but skips the actual adapter
|
|
19
|
+
* registration step. The caller is then responsible for ordering and
|
|
20
|
+
* installing the collected routes via `registerResolvedRoute`.
|
|
21
|
+
* Defaults to `false`.
|
|
22
|
+
*/
|
|
23
|
+
deferRegistration?: boolean;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Logger, type RouteConflictPolicy, type VersioningOptions } from '@nestjs/common';
|
|
2
|
+
import { ResolvedRoute } from './interfaces/resolved-route.interface.js';
|
|
3
|
+
import { RouteConflict } from './interfaces/route-conflict.interface.js';
|
|
4
|
+
type SegmentKind = 'literal' | 'param' | 'wildcard';
|
|
5
|
+
interface PathSegment {
|
|
6
|
+
kind: SegmentKind;
|
|
7
|
+
value: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Static utility class that detects overlapping HTTP routes and reports
|
|
11
|
+
* them according to a per-kind policy. Stateless — every method takes
|
|
12
|
+
* everything it needs as parameters.
|
|
13
|
+
*/
|
|
14
|
+
export declare class RouteConflictDetector {
|
|
15
|
+
/**
|
|
16
|
+
* Strips the leading `:` / `*` marker (if present) and tags each
|
|
17
|
+
* segment as a literal, named param, or named wildcard. Supports both
|
|
18
|
+
* bare named wildcards (`*path`) and adapter-normalized path-to-regexp
|
|
19
|
+
* wildcard groups (`{*path}`).
|
|
20
|
+
*/
|
|
21
|
+
static tokenizePath(rawPath: string): PathSegment[];
|
|
22
|
+
/**
|
|
23
|
+
* Decides whether two paths can match the same incoming request, given
|
|
24
|
+
* only their declared patterns (no host/method/version considered).
|
|
25
|
+
*/
|
|
26
|
+
static pathsCanOverlap(leftPath: string, rightPath: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Walks every unique pair of resolved routes and produces a conflict
|
|
29
|
+
* record for each pair whose (method, host, version, path) tuples can
|
|
30
|
+
* collide at runtime.
|
|
31
|
+
*/
|
|
32
|
+
static detect(routes: ResolvedRoute[], versioningOptions: VersioningOptions | undefined): RouteConflict[];
|
|
33
|
+
/**
|
|
34
|
+
* Applies the per-kind policy to a set of conflicts: silences `'off'`,
|
|
35
|
+
* logs `'warn'` once per conflict, and aggregates every `'error'`-level
|
|
36
|
+
* conflict into a single `RouteConflictException`.
|
|
37
|
+
*/
|
|
38
|
+
static handle(conflicts: RouteConflict[], policy: RouteConflictPolicy | undefined, logger: Logger): void;
|
|
39
|
+
/**
|
|
40
|
+
* Removes shadow conflicts that specificity sorting has already resolved.
|
|
41
|
+
*
|
|
42
|
+
* When `routeResolutionStrategy: 'specificity'` is active, the sort
|
|
43
|
+
* promotes more-specific routes ahead of less-specific ones. A shadow
|
|
44
|
+
* where the sort promoted the winner (it was declared *later* but sorted
|
|
45
|
+
* *first*) is handled correctly at runtime — the more-specific route is
|
|
46
|
+
* registered first and handles its requests while the less-specific route
|
|
47
|
+
* handles the rest. Retaining such a conflict would cause `shadow: 'error'`
|
|
48
|
+
* to abort an application whose routes actually work as intended.
|
|
49
|
+
*
|
|
50
|
+
* Shadows where the winner was already first in declaration order (the
|
|
51
|
+
* sort did not swap them) are genuine and are kept unchanged. Duplicate
|
|
52
|
+
* conflicts are always kept.
|
|
53
|
+
*
|
|
54
|
+
* @param conflicts Conflicts detected on the sorted route list.
|
|
55
|
+
* @param declarationOrder Routes in their original declaration order
|
|
56
|
+
* (i.e. before specificity sorting was applied).
|
|
57
|
+
*/
|
|
58
|
+
static filterSortResolvedShadows(conflicts: RouteConflict[], declarationOrder: ResolvedRoute[]): RouteConflict[];
|
|
59
|
+
private static segmentsCanOverlap;
|
|
60
|
+
private static methodsCanOverlap;
|
|
61
|
+
private static versionsCanOverlap;
|
|
62
|
+
private static hostsCanOverlap;
|
|
63
|
+
private static hostValuesCanMatchSameRequest;
|
|
64
|
+
private static routesAreIdentical;
|
|
65
|
+
private static hostsAreIdentical;
|
|
66
|
+
private static hostValuesAreIdentical;
|
|
67
|
+
private static versionsAreIdentical;
|
|
68
|
+
private static forEachUniquePair;
|
|
69
|
+
private static describeConflict;
|
|
70
|
+
}
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { RequestMethod, VERSION_NEUTRAL, VersioningType, } from '@nestjs/common';
|
|
2
|
+
import { RouteConflictException } from '../errors/exceptions/route-conflict.exception.js';
|
|
3
|
+
import { DUPLICATE_ROUTE_MESSAGE, SHADOWED_ROUTE_MESSAGE, } from '../errors/messages.js';
|
|
4
|
+
/**
|
|
5
|
+
* Static utility class that detects overlapping HTTP routes and reports
|
|
6
|
+
* them according to a per-kind policy. Stateless — every method takes
|
|
7
|
+
* everything it needs as parameters.
|
|
8
|
+
*/
|
|
9
|
+
export class RouteConflictDetector {
|
|
10
|
+
/**
|
|
11
|
+
* Strips the leading `:` / `*` marker (if present) and tags each
|
|
12
|
+
* segment as a literal, named param, or named wildcard. Supports both
|
|
13
|
+
* bare named wildcards (`*path`) and adapter-normalized path-to-regexp
|
|
14
|
+
* wildcard groups (`{*path}`).
|
|
15
|
+
*/
|
|
16
|
+
static tokenizePath(rawPath) {
|
|
17
|
+
const segments = [];
|
|
18
|
+
rawPath
|
|
19
|
+
.split('/')
|
|
20
|
+
.filter(rawSegment => rawSegment.length > 0)
|
|
21
|
+
.forEach(rawSegment => {
|
|
22
|
+
if (rawSegment.startsWith('*')) {
|
|
23
|
+
segments.push({ kind: 'wildcard', value: rawSegment.slice(1) });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (rawSegment.startsWith('{*') && rawSegment.endsWith('}')) {
|
|
27
|
+
segments.push({
|
|
28
|
+
kind: 'wildcard',
|
|
29
|
+
value: rawSegment.slice(2, -1),
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (rawSegment.startsWith(':')) {
|
|
34
|
+
segments.push({ kind: 'param', value: rawSegment.slice(1) });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
segments.push({ kind: 'literal', value: rawSegment });
|
|
38
|
+
});
|
|
39
|
+
return segments;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Decides whether two paths can match the same incoming request, given
|
|
43
|
+
* only their declared patterns (no host/method/version considered).
|
|
44
|
+
*/
|
|
45
|
+
static pathsCanOverlap(leftPath, rightPath) {
|
|
46
|
+
const leftSegments = RouteConflictDetector.tokenizePath(leftPath);
|
|
47
|
+
const rightSegments = RouteConflictDetector.tokenizePath(rightPath);
|
|
48
|
+
const leftEndsInWildcard = leftSegments[leftSegments.length - 1]?.kind === 'wildcard';
|
|
49
|
+
const rightEndsInWildcard = rightSegments[rightSegments.length - 1]?.kind === 'wildcard';
|
|
50
|
+
// A named wildcard like `*path` requires at least one matched segment,
|
|
51
|
+
// so only the *shorter* side's trailing wildcard can absorb the
|
|
52
|
+
// difference. If the longer side has the wildcard, the other side
|
|
53
|
+
// simply does not have enough segments to ever reach that position.
|
|
54
|
+
if (leftSegments.length !== rightSegments.length) {
|
|
55
|
+
const shorterEndsInWildcard = leftSegments.length < rightSegments.length
|
|
56
|
+
? leftEndsInWildcard
|
|
57
|
+
: rightEndsInWildcard;
|
|
58
|
+
if (!shorterEndsInWildcard) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const sharedLength = Math.min(leftSegments.length, rightSegments.length);
|
|
63
|
+
let canOverlap = true;
|
|
64
|
+
leftSegments.slice(0, sharedLength).forEach((leftSegment, segmentIndex) => {
|
|
65
|
+
if (!canOverlap)
|
|
66
|
+
return;
|
|
67
|
+
if (!RouteConflictDetector.segmentsCanOverlap(leftSegment, rightSegments[segmentIndex])) {
|
|
68
|
+
canOverlap = false;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return canOverlap;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Walks every unique pair of resolved routes and produces a conflict
|
|
75
|
+
* record for each pair whose (method, host, version, path) tuples can
|
|
76
|
+
* collide at runtime.
|
|
77
|
+
*/
|
|
78
|
+
static detect(routes, versioningOptions) {
|
|
79
|
+
const conflicts = [];
|
|
80
|
+
RouteConflictDetector.forEachUniquePair(routes, (earlierRoute, laterRoute) => {
|
|
81
|
+
if (!RouteConflictDetector.methodsCanOverlap(earlierRoute.method, laterRoute.method)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!RouteConflictDetector.versionsCanOverlap(earlierRoute.version, laterRoute.version, versioningOptions)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!RouteConflictDetector.hostsCanOverlap(earlierRoute.host, laterRoute.host)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!RouteConflictDetector.pathsCanOverlap(earlierRoute.path, laterRoute.path)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const isIdentical = RouteConflictDetector.routesAreIdentical(earlierRoute, laterRoute, versioningOptions);
|
|
94
|
+
conflicts.push({
|
|
95
|
+
winner: earlierRoute,
|
|
96
|
+
shadowed: laterRoute,
|
|
97
|
+
kind: isIdentical ? 'duplicate' : 'shadow',
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
return conflicts;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Applies the per-kind policy to a set of conflicts: silences `'off'`,
|
|
104
|
+
* logs `'warn'` once per conflict, and aggregates every `'error'`-level
|
|
105
|
+
* conflict into a single `RouteConflictException`.
|
|
106
|
+
*/
|
|
107
|
+
static handle(conflicts, policy, logger) {
|
|
108
|
+
if (conflicts.length === 0 || policy === undefined)
|
|
109
|
+
return;
|
|
110
|
+
const errorMessages = [];
|
|
111
|
+
conflicts.forEach(conflict => {
|
|
112
|
+
const policyForKind = policy[conflict.kind] ?? 'off';
|
|
113
|
+
if (policyForKind === 'off')
|
|
114
|
+
return;
|
|
115
|
+
const message = RouteConflictDetector.describeConflict(conflict);
|
|
116
|
+
if (policyForKind === 'warn') {
|
|
117
|
+
logger.warn(message);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
errorMessages.push(message);
|
|
121
|
+
});
|
|
122
|
+
if (errorMessages.length > 0) {
|
|
123
|
+
throw new RouteConflictException(errorMessages);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Removes shadow conflicts that specificity sorting has already resolved.
|
|
128
|
+
*
|
|
129
|
+
* When `routeResolutionStrategy: 'specificity'` is active, the sort
|
|
130
|
+
* promotes more-specific routes ahead of less-specific ones. A shadow
|
|
131
|
+
* where the sort promoted the winner (it was declared *later* but sorted
|
|
132
|
+
* *first*) is handled correctly at runtime — the more-specific route is
|
|
133
|
+
* registered first and handles its requests while the less-specific route
|
|
134
|
+
* handles the rest. Retaining such a conflict would cause `shadow: 'error'`
|
|
135
|
+
* to abort an application whose routes actually work as intended.
|
|
136
|
+
*
|
|
137
|
+
* Shadows where the winner was already first in declaration order (the
|
|
138
|
+
* sort did not swap them) are genuine and are kept unchanged. Duplicate
|
|
139
|
+
* conflicts are always kept.
|
|
140
|
+
*
|
|
141
|
+
* @param conflicts Conflicts detected on the sorted route list.
|
|
142
|
+
* @param declarationOrder Routes in their original declaration order
|
|
143
|
+
* (i.e. before specificity sorting was applied).
|
|
144
|
+
*/
|
|
145
|
+
static filterSortResolvedShadows(conflicts, declarationOrder) {
|
|
146
|
+
const declarationIndex = new Map(declarationOrder.map((route, idx) => [route, idx]));
|
|
147
|
+
return conflicts.filter(conflict => {
|
|
148
|
+
if (conflict.kind !== 'shadow')
|
|
149
|
+
return true;
|
|
150
|
+
const winnerDeclIdx = declarationIndex.get(conflict.winner) ?? -1;
|
|
151
|
+
const shadowedDeclIdx = declarationIndex.get(conflict.shadowed) ?? -1;
|
|
152
|
+
// The sort promoted the winner (declared later, but sorted to the
|
|
153
|
+
// front because it is more specific). The shadow is resolved at
|
|
154
|
+
// runtime — drop it. Keep only genuine shadows where the winner was
|
|
155
|
+
// already first in declaration order.
|
|
156
|
+
return winnerDeclIdx < shadowedDeclIdx;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
static segmentsCanOverlap(leftSegment, rightSegment) {
|
|
160
|
+
if (leftSegment.kind === 'wildcard' || rightSegment.kind === 'wildcard') {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
if (leftSegment.kind === 'param' || rightSegment.kind === 'param') {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
return leftSegment.value === rightSegment.value;
|
|
167
|
+
}
|
|
168
|
+
static methodsCanOverlap(leftMethod, rightMethod) {
|
|
169
|
+
if (leftMethod === RequestMethod.ALL || rightMethod === RequestMethod.ALL) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return leftMethod === rightMethod;
|
|
173
|
+
}
|
|
174
|
+
static versionsCanOverlap(leftVersion, rightVersion, versioningOptions) {
|
|
175
|
+
if (!versioningOptions)
|
|
176
|
+
return true;
|
|
177
|
+
if (versioningOptions.type === VersioningType.URI)
|
|
178
|
+
return true;
|
|
179
|
+
const leftMatchesAnyVersion = leftVersion === undefined || leftVersion === VERSION_NEUTRAL;
|
|
180
|
+
const rightMatchesAnyVersion = rightVersion === undefined || rightVersion === VERSION_NEUTRAL;
|
|
181
|
+
if (leftMatchesAnyVersion || rightMatchesAnyVersion)
|
|
182
|
+
return true;
|
|
183
|
+
const leftValues = Array.isArray(leftVersion) ? leftVersion : [leftVersion];
|
|
184
|
+
const rightValues = Array.isArray(rightVersion)
|
|
185
|
+
? rightVersion
|
|
186
|
+
: [rightVersion];
|
|
187
|
+
return leftValues.some(versionValue => rightValues.includes(versionValue));
|
|
188
|
+
}
|
|
189
|
+
static hostsCanOverlap(leftHost, rightHost) {
|
|
190
|
+
if (leftHost === undefined || rightHost === undefined)
|
|
191
|
+
return true;
|
|
192
|
+
const leftHosts = Array.isArray(leftHost) ? leftHost : [leftHost];
|
|
193
|
+
const rightHosts = Array.isArray(rightHost) ? rightHost : [rightHost];
|
|
194
|
+
return leftHosts.some(leftValue => rightHosts.some(rightValue => RouteConflictDetector.hostValuesCanMatchSameRequest(leftValue, rightValue)));
|
|
195
|
+
}
|
|
196
|
+
static hostValuesCanMatchSameRequest(leftValue, rightValue) {
|
|
197
|
+
const leftIsRegExp = leftValue instanceof RegExp;
|
|
198
|
+
const rightIsRegExp = rightValue instanceof RegExp;
|
|
199
|
+
if (leftIsRegExp && rightIsRegExp)
|
|
200
|
+
return true;
|
|
201
|
+
// Reset lastIndex before calling test() to guard against RegExps with the
|
|
202
|
+
// `g` or `y` flags: those are stateful and would produce inconsistent
|
|
203
|
+
// results (false negatives) when the same instance is reused across the
|
|
204
|
+
// multiple pair comparisons that a single detect() run performs.
|
|
205
|
+
if (leftIsRegExp) {
|
|
206
|
+
leftValue.lastIndex = 0;
|
|
207
|
+
return leftValue.test(rightValue);
|
|
208
|
+
}
|
|
209
|
+
if (rightIsRegExp) {
|
|
210
|
+
rightValue.lastIndex = 0;
|
|
211
|
+
return rightValue.test(leftValue);
|
|
212
|
+
}
|
|
213
|
+
return leftValue === rightValue;
|
|
214
|
+
}
|
|
215
|
+
static routesAreIdentical(leftRoute, rightRoute, versioningOptions) {
|
|
216
|
+
return (leftRoute.method === rightRoute.method &&
|
|
217
|
+
leftRoute.path === rightRoute.path &&
|
|
218
|
+
RouteConflictDetector.hostsAreIdentical(leftRoute.host, rightRoute.host) &&
|
|
219
|
+
RouteConflictDetector.versionsAreIdentical(leftRoute.version, rightRoute.version, versioningOptions));
|
|
220
|
+
}
|
|
221
|
+
static hostsAreIdentical(leftHost, rightHost) {
|
|
222
|
+
if (leftHost === undefined && rightHost === undefined)
|
|
223
|
+
return true;
|
|
224
|
+
if (leftHost === undefined || rightHost === undefined)
|
|
225
|
+
return false;
|
|
226
|
+
const leftHosts = Array.isArray(leftHost) ? leftHost : [leftHost];
|
|
227
|
+
const rightHosts = Array.isArray(rightHost) ? rightHost : [rightHost];
|
|
228
|
+
if (leftHosts.length !== rightHosts.length)
|
|
229
|
+
return false;
|
|
230
|
+
// Order-insensitive set comparison: ['a', 'b'] and ['b', 'a']
|
|
231
|
+
// describe the same allowed-host set, so they are identical for
|
|
232
|
+
// duplicate-classification purposes.
|
|
233
|
+
return leftHosts.every(leftValue => rightHosts.some(rightValue => RouteConflictDetector.hostValuesAreIdentical(leftValue, rightValue)));
|
|
234
|
+
}
|
|
235
|
+
static hostValuesAreIdentical(leftValue, rightValue) {
|
|
236
|
+
if (leftValue instanceof RegExp && rightValue instanceof RegExp) {
|
|
237
|
+
return (leftValue.source === rightValue.source &&
|
|
238
|
+
leftValue.flags === rightValue.flags);
|
|
239
|
+
}
|
|
240
|
+
return leftValue === rightValue;
|
|
241
|
+
}
|
|
242
|
+
static versionsAreIdentical(leftVersion, rightVersion, versioningOptions) {
|
|
243
|
+
// When versioning is not configured (or URI-based, where the
|
|
244
|
+
// version is encoded in the path), version metadata does not
|
|
245
|
+
// gate request matching at runtime, so two routes that differ
|
|
246
|
+
// only in their declared `version` are runtime duplicates.
|
|
247
|
+
if (!versioningOptions || versioningOptions.type === VersioningType.URI) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
if (leftVersion === rightVersion)
|
|
251
|
+
return true;
|
|
252
|
+
const leftValues = Array.isArray(leftVersion) ? leftVersion : [leftVersion];
|
|
253
|
+
const rightValues = Array.isArray(rightVersion)
|
|
254
|
+
? rightVersion
|
|
255
|
+
: [rightVersion];
|
|
256
|
+
if (leftValues.length !== rightValues.length)
|
|
257
|
+
return false;
|
|
258
|
+
return leftValues.every(value => rightValues.includes(value));
|
|
259
|
+
}
|
|
260
|
+
static forEachUniquePair(items, visit) {
|
|
261
|
+
items.forEach((leftItem, leftIndex) => {
|
|
262
|
+
items.slice(leftIndex + 1).forEach(rightItem => {
|
|
263
|
+
visit(leftItem, rightItem);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
static describeConflict(conflict) {
|
|
268
|
+
const method = RequestMethod[conflict.winner.method];
|
|
269
|
+
const winnerLabel = `${conflict.winner.instanceWrapper.name}#${conflict.winner.methodName}`;
|
|
270
|
+
const shadowedLabel = `${conflict.shadowed.instanceWrapper.name}#${conflict.shadowed.methodName}`;
|
|
271
|
+
if (conflict.kind === 'duplicate') {
|
|
272
|
+
return DUPLICATE_ROUTE_MESSAGE(method, conflict.winner.path, winnerLabel, shadowedLabel);
|
|
273
|
+
}
|
|
274
|
+
return SHADOWED_ROUTE_MESSAGE(method, conflict.shadowed.path, shadowedLabel, conflict.winner.path, winnerLabel);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ResolvedRoute } from './interfaces/resolved-route.interface.js';
|
|
2
|
+
/**
|
|
3
|
+
* Static utility class that orders resolved routes by specificity so the
|
|
4
|
+
* underlying HTTP adapter registers more specific patterns first.
|
|
5
|
+
* Stateless — every method takes everything it needs as parameters.
|
|
6
|
+
*/
|
|
7
|
+
export declare class RouteSpecificitySorter {
|
|
8
|
+
/**
|
|
9
|
+
* Lower rank means more specific. A literal segment beats a named
|
|
10
|
+
* param, which beats a named wildcard. A position that is absent on
|
|
11
|
+
* one side is the least specific of all (it means the path is shorter
|
|
12
|
+
* at that point).
|
|
13
|
+
*/
|
|
14
|
+
private static readonly SEGMENT_KIND_RANK;
|
|
15
|
+
/**
|
|
16
|
+
* Returns a new array of routes sorted from most-specific to
|
|
17
|
+
* least-specific. Routes that tie on specificity keep their original
|
|
18
|
+
* declaration order.
|
|
19
|
+
*/
|
|
20
|
+
static sort(routes: ResolvedRoute[]): ResolvedRoute[];
|
|
21
|
+
private static comparePathSpecificity;
|
|
22
|
+
private static rankSegmentByKind;
|
|
23
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { RouteConflictDetector } from './route-conflict-detector.js';
|
|
2
|
+
/**
|
|
3
|
+
* Static utility class that orders resolved routes by specificity so the
|
|
4
|
+
* underlying HTTP adapter registers more specific patterns first.
|
|
5
|
+
* Stateless — every method takes everything it needs as parameters.
|
|
6
|
+
*/
|
|
7
|
+
export class RouteSpecificitySorter {
|
|
8
|
+
/**
|
|
9
|
+
* Lower rank means more specific. A literal segment beats a named
|
|
10
|
+
* param, which beats a named wildcard. A position that is absent on
|
|
11
|
+
* one side is the least specific of all (it means the path is shorter
|
|
12
|
+
* at that point).
|
|
13
|
+
*/
|
|
14
|
+
static SEGMENT_KIND_RANK = {
|
|
15
|
+
literal: 0,
|
|
16
|
+
param: 1,
|
|
17
|
+
wildcard: 2,
|
|
18
|
+
missing: 3,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Returns a new array of routes sorted from most-specific to
|
|
22
|
+
* least-specific. Routes that tie on specificity keep their original
|
|
23
|
+
* declaration order.
|
|
24
|
+
*/
|
|
25
|
+
static sort(routes) {
|
|
26
|
+
const decoratedRoutes = routes.map((route, declarationIndex) => ({
|
|
27
|
+
route,
|
|
28
|
+
declarationIndex,
|
|
29
|
+
}));
|
|
30
|
+
decoratedRoutes.sort((leftEntry, rightEntry) => {
|
|
31
|
+
const specificityDelta = RouteSpecificitySorter.comparePathSpecificity(leftEntry.route.path, rightEntry.route.path);
|
|
32
|
+
if (specificityDelta !== 0)
|
|
33
|
+
return specificityDelta;
|
|
34
|
+
return leftEntry.declarationIndex - rightEntry.declarationIndex;
|
|
35
|
+
});
|
|
36
|
+
return decoratedRoutes.map(decoratedEntry => decoratedEntry.route);
|
|
37
|
+
}
|
|
38
|
+
static comparePathSpecificity(leftPath, rightPath) {
|
|
39
|
+
const leftSegments = RouteConflictDetector.tokenizePath(leftPath);
|
|
40
|
+
const rightSegments = RouteConflictDetector.tokenizePath(rightPath);
|
|
41
|
+
const longestPathLength = Math.max(leftSegments.length, rightSegments.length);
|
|
42
|
+
let specificityDelta = 0;
|
|
43
|
+
Array.from({ length: longestPathLength }).forEach((_, segmentIndex) => {
|
|
44
|
+
if (specificityDelta !== 0)
|
|
45
|
+
return;
|
|
46
|
+
const leftKind = leftSegments[segmentIndex]?.kind ?? 'missing';
|
|
47
|
+
const rightKind = rightSegments[segmentIndex]?.kind ?? 'missing';
|
|
48
|
+
const leftRank = RouteSpecificitySorter.rankSegmentByKind(leftKind);
|
|
49
|
+
const rightRank = RouteSpecificitySorter.rankSegmentByKind(rightKind);
|
|
50
|
+
if (leftRank !== rightRank) {
|
|
51
|
+
specificityDelta = leftRank - rightRank;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return specificityDelta;
|
|
55
|
+
}
|
|
56
|
+
static rankSegmentByKind(kind) {
|
|
57
|
+
return RouteSpecificitySorter.SEGMENT_KIND_RANK[kind];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -26,7 +26,7 @@ class RouterExecutionContext {
|
|
|
26
26
|
}
|
|
27
27
|
create(instance, callback, methodName, moduleKey, requestMethod, contextId = constants_2.STATIC_CONTEXT, inquirerId) {
|
|
28
28
|
const contextType = 'http';
|
|
29
|
-
const { argsLength, fnHandleResponse, paramtypes, getParamsMetadata, httpStatusCode, responseHeaders, hasCustomHeaders, } = this.getMetadata(instance, callback, methodName, moduleKey, requestMethod, contextType);
|
|
29
|
+
const { argsLength, fnHandleResponse, isSseHandler, paramtypes, getParamsMetadata, httpStatusCode, responseHeaders, hasCustomHeaders, } = this.getMetadata(instance, callback, methodName, moduleKey, requestMethod, contextType);
|
|
30
30
|
const paramsOptions = this.contextUtils.mergeParamsMetatypes(getParamsMetadata(moduleKey, contextId, inquirerId), paramtypes);
|
|
31
31
|
const pipes = this.pipesContextCreator.create(instance, callback, moduleKey, contextId, inquirerId);
|
|
32
32
|
const guards = this.guardsContextCreator.create(instance, callback, moduleKey, contextId, inquirerId);
|
|
@@ -43,7 +43,8 @@ class RouterExecutionContext {
|
|
|
43
43
|
this.responseController.setStatus(res, httpStatusCode);
|
|
44
44
|
hasCustomHeaders &&
|
|
45
45
|
this.responseController.setHeaders(res, responseHeaders);
|
|
46
|
-
const
|
|
46
|
+
const resultOrDeferred = this.interceptorsConsumer.intercept(interceptors, [req, res, next], instance, callback, handler(args, req, res, next), contextType);
|
|
47
|
+
const result = isSseHandler ? resultOrDeferred : await resultOrDeferred;
|
|
47
48
|
await fnHandleResponse(result, res, req);
|
|
48
49
|
};
|
|
49
50
|
}
|
|
@@ -62,6 +63,7 @@ class RouterExecutionContext {
|
|
|
62
63
|
const isResponseHandled = this.isResponseHandled(instance, methodName, paramsMetadata);
|
|
63
64
|
const httpRedirectResponse = this.reflectRedirect(callback);
|
|
64
65
|
const fnHandleResponse = this.createHandleResponseFn(callback, isResponseHandled, httpRedirectResponse);
|
|
66
|
+
const isSseHandler = !!this.reflectSse(callback);
|
|
65
67
|
const httpCode = this.reflectHttpStatusCode(callback);
|
|
66
68
|
const httpStatusCode = httpCode
|
|
67
69
|
? httpCode
|
|
@@ -71,6 +73,7 @@ class RouterExecutionContext {
|
|
|
71
73
|
const handlerMetadata = {
|
|
72
74
|
argsLength,
|
|
73
75
|
fnHandleResponse,
|
|
76
|
+
isSseHandler,
|
|
74
77
|
paramtypes,
|
|
75
78
|
getParamsMetadata,
|
|
76
79
|
httpStatusCode,
|
|
@@ -53,74 +53,102 @@ class RouterResponseController {
|
|
|
53
53
|
if (response.writableEnded) {
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
|
-
const observableResult = await Promise.resolve(result);
|
|
57
|
-
this.assertObservable(observableResult);
|
|
58
56
|
const stream = new sse_stream_1.SseStream(request);
|
|
59
57
|
const statusCode = options?.statusCode ??
|
|
60
58
|
response.statusCode ??
|
|
61
59
|
200;
|
|
62
|
-
stream.pipe(response, {
|
|
63
|
-
additionalHeaders: options?.additionalHeaders,
|
|
64
|
-
statusCode,
|
|
65
|
-
});
|
|
66
60
|
return new Promise((resolve, reject) => {
|
|
67
61
|
let settled = false;
|
|
62
|
+
let subscription;
|
|
63
|
+
const cleanup = () => request.removeListener('close', onClose);
|
|
68
64
|
const onClose = () => {
|
|
65
|
+
if (settled) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
69
68
|
settled = true;
|
|
70
|
-
|
|
69
|
+
cleanup();
|
|
70
|
+
subscription?.unsubscribe();
|
|
71
71
|
if (!stream.writableEnded) {
|
|
72
72
|
stream.end();
|
|
73
73
|
}
|
|
74
74
|
response.end();
|
|
75
75
|
resolve();
|
|
76
76
|
};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return { data: message };
|
|
83
|
-
}), (0, operators_1.concatMap)(message => new Promise(resolve => stream.writeMessage(message, () => resolve()))), (0, operators_1.catchError)(err => {
|
|
84
|
-
if (!stream.headersCommitted) {
|
|
85
|
-
throw err;
|
|
77
|
+
request.once('close', onClose);
|
|
78
|
+
Promise.resolve(result)
|
|
79
|
+
.then(observableResult => {
|
|
80
|
+
if (settled) {
|
|
81
|
+
return;
|
|
86
82
|
}
|
|
87
|
-
|
|
88
|
-
stream.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
83
|
+
this.assertObservable(observableResult);
|
|
84
|
+
stream.pipe(response, {
|
|
85
|
+
additionalHeaders: options?.additionalHeaders,
|
|
86
|
+
statusCode,
|
|
92
87
|
});
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
settled = true;
|
|
98
|
-
request.removeListener('close', onClose);
|
|
99
|
-
if (!stream.writableEnded) {
|
|
100
|
-
stream.end();
|
|
88
|
+
subscription = observableResult
|
|
89
|
+
.pipe((0, operators_1.map)((message) => {
|
|
90
|
+
if ((0, shared_utils_1.isObject)(message)) {
|
|
91
|
+
return message;
|
|
101
92
|
}
|
|
102
|
-
|
|
103
|
-
},
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
request.removeListener('close', onClose);
|
|
107
|
-
if (!stream.writableEnded) {
|
|
108
|
-
stream.end();
|
|
93
|
+
return { data: message };
|
|
94
|
+
}), (0, operators_1.concatMap)(message => new Promise(resolve => stream.writeMessage(message, () => resolve()))), (0, operators_1.catchError)(err => {
|
|
95
|
+
if (!stream.headersCommitted) {
|
|
96
|
+
throw err;
|
|
109
97
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
98
|
+
const data = err instanceof Error ? err.message : err;
|
|
99
|
+
stream.writeMessage({ type: 'error', data }, writeError => {
|
|
100
|
+
if (writeError) {
|
|
101
|
+
this.logger.error(writeError);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return rxjs_1.EMPTY;
|
|
105
|
+
}))
|
|
106
|
+
.subscribe({
|
|
107
|
+
error: err => {
|
|
108
|
+
if (settled) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
settled = true;
|
|
112
|
+
cleanup();
|
|
113
|
+
if (!stream.writableEnded) {
|
|
114
|
+
stream.end();
|
|
115
|
+
}
|
|
116
|
+
reject(err);
|
|
117
|
+
},
|
|
118
|
+
complete: () => {
|
|
119
|
+
if (settled) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
settled = true;
|
|
123
|
+
cleanup();
|
|
124
|
+
if (!stream.writableEnded) {
|
|
125
|
+
stream.end();
|
|
126
|
+
}
|
|
127
|
+
resolve();
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
// Commit SSE headers on the next macrotask. Pipe validation errors
|
|
131
|
+
// propagate through microtasks (which complete before macrotasks),
|
|
132
|
+
// so if the lifecycle errored, `settled` is already true and we
|
|
133
|
+
// skip the write. Otherwise headers are sent immediately rather
|
|
134
|
+
// than waiting for the first Observable emission.
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
if (!settled) {
|
|
137
|
+
stream.commitHeaders();
|
|
138
|
+
}
|
|
139
|
+
}, 0);
|
|
140
|
+
})
|
|
141
|
+
.catch(err => {
|
|
142
|
+
if (settled) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
settled = true;
|
|
146
|
+
cleanup();
|
|
147
|
+
if (!stream.writableEnded) {
|
|
148
|
+
stream.end();
|
|
121
149
|
}
|
|
122
|
-
|
|
123
|
-
|
|
150
|
+
reject(err);
|
|
151
|
+
});
|
|
124
152
|
});
|
|
125
153
|
}
|
|
126
154
|
assertObservable(value) {
|