-
-
Notifications
You must be signed in to change notification settings - Fork 8k
feat(ws): Support for dynamic path params in websocket, add tests #15488
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat(ws): Support for dynamic path params in websocket, add tests #15488
Conversation
Hi @team @micalevisk @kamilmysliwiec I hope you're all doing well! I just wanted to bring this to your attention when you have a moment. I've been using NestJS for quite a while now and have built numerous backend applications with it - it's been an absolute game-changer for my development workflow. The framework's elegance and powerful features have made backend development so much more enjoyable and productive. I truly appreciate all the incredible work you've put into this project and the continuous improvements you keep delivering to the community. Your dedication to maintaining such a high-quality framework doesn't go unnoticed. Please feel free to take a look when your schedule permits. Thank you so much for all your contributions and for making NestJS the amazing framework it is today. I look forward to your feedback! |
Pull Request Test Coverage Report for Build ec6d52bd-26c3-40b0-a37f-98f0df3fe2c6Details
💛 - Coveralls |
Hope you're doing well! Just a gentle ping on this PR when you get a chance. Happy to collaborate and make this better. Thanks as always! |
This commit introduces significant performance optimizations to the Key Changes
Performance OptimizationsCore Optimization: Indexed Path MatchingBefore (Linear Search): // O(n) - Iterate through all servers for each connection
for (const wsServer of wsServersCollection) {
if (pathname === wsServer.path || wsServer.pathRegexp?.test(pathname)) {
// Match found
}
} After (Indexed Lookup): // O(1) for static paths, optimized O(k) for dynamic paths
interface PathMatcher {
staticPaths: Map<string, WsServerWithPath[]>; // O(1) lookup
dynamicPaths: Array<{ // Sorted by complexity
server: WsServerWithPath;
pathRegexp: RegExp;
pathKeys: Key[];
}>;
} Performance Benchmark Results
This is script origin output:
Origin performance script/**
* Path matching performance benchmark test
*/
import { WsAdapter } from '../../adapters/ws-adapter';
import { Test } from '@nestjs/testing';
import { INestApplicationContext } from '@nestjs/common';
interface BenchmarkResult {
operation: string;
iterations: number;
totalTime: number;
averageTime: number;
opsPerSecond: number;
}
describe('WebSocket Path Matching Performance', () => {
let adapter: WsAdapter;
let app: INestApplicationContext;
beforeEach(async () => {
const module = await Test.createTestingModule({}).compile();
app = module.createNestApplication();
adapter = new WsAdapter(app);
});
afterEach(async () => {
await adapter.dispose();
await app.close();
});
it('should demonstrate performance improvement with optimized path matcher', async () => {
const port = 3001;
// Create multiple WebSocket servers with mixed static and dynamic paths
const staticPaths = [
'/api/v1/chat',
'/api/v1/notifications',
'/api/v1/status',
'/api/v2/chat',
'/api/v2/notifications',
];
const dynamicPaths = [
'/chat/:roomId/socket',
'/game/:gameId/room/:roomId/socket',
'/user/:userId/notifications',
'/api/v1/room/:roomId/user/:userId',
'/api/v2/game/:gameId/player/:playerId/stream',
];
// Register static paths
for (const path of staticPaths) {
const server = adapter.create(port, { path });
// Simulate server registration without actually starting
}
// Register dynamic paths
for (const path of dynamicPaths) {
const server = adapter.create(port, { path });
// Simulate server registration without actually starting
}
// Test paths to match
const testPaths = [
'/api/v1/chat', // Static match
'/chat/room123/socket', // Dynamic match
'/game/game456/room/room789/socket', // Complex dynamic match
'/user/user123/notifications', // Dynamic match
'/api/v2/game/game999/player/player888/stream', // Complex dynamic match
'/nonexistent/path', // No match
];
console.log('\n WebSocket Path Matching Performance Benchmark');
console.log('==================================================');
// Get the path matcher (this will create the optimized index)
const pathMatcher = (adapter as any).getOrCreatePathMatcher(port);
console.log(`\n Path Matcher Statistics:`);
console.log(` Static paths: ${pathMatcher.staticPaths.size}`);
console.log(` Dynamic paths: ${pathMatcher.dynamicPaths.length}`);
// Benchmark path matching
const iterations = 10000;
for (const testPath of testPaths) {
const result = await benchmarkPathMatching(
adapter as any,
pathMatcher,
testPath,
iterations,
);
printBenchmarkResult(result);
}
console.log('\n Performance optimization verified!');
console.log('Static paths now use O(1) Map lookup');
console.log('Dynamic paths are sorted by complexity for faster matching');
});
});
async function benchmarkPathMatching(
adapter: any,
pathMatcher: any,
testPath: string,
iterations: number,
): Promise<BenchmarkResult> {
const startTime = process.hrtime.bigint();
let matchCount = 0;
for (let i = 0; i < iterations; i++) {
const result = adapter.matchPath(testPath, pathMatcher);
if (result) matchCount++;
}
const endTime = process.hrtime.bigint();
const totalTime = Number(endTime - startTime) / 1000000; // Convert to milliseconds
return {
operation: `Match "${testPath}" (${matchCount > 0 ? 'found' : 'not found'})`,
iterations,
totalTime,
averageTime: totalTime / iterations,
opsPerSecond: Math.round(iterations / (totalTime / 1000)),
};
}
function printBenchmarkResult(result: BenchmarkResult): void {
console.log(`\n🎯 ${result.operation}`);
console.log(` Iterations: ${result.iterations.toLocaleString()}`);
console.log(` Total time: ${result.totalTime.toFixed(2)}ms`);
console.log(
` Average time: ${result.averageTime.toFixed(6)}ms per operation`,
);
console.log(
` Performance: ${result.opsPerSecond.toLocaleString()} operations/second`,
);
} |
Hi @micalevisk Thanks for the positive reaction! I noticed you've shown interest in this PR. Would you be able to provide a code review when you have time? I'd really appreciate any feedback or suggestions you might have. cc @kamilmysliwiec |
HI @micalevisk |
Hope you're doing well. I Just wanted to check if there's anything blocking this PR or if there are any concerns I should address. No pressure, just want to make sure it's still on the radar. No rush at all - I know everyone's busy! If there's anything I can clarify or improve, please let me know. I'm happy to make any adjustments needed. Thanks for your time! |
PR Checklist
Please check if your PR fulfills the following requirements:
The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
Tests for the changes have been added (for bug fixes / features)
Docs have been added / updated (for bug fixes / features)
The documentation has been completed and the branch is ready, but the PR hasn't been submitted yet. I plan to discuss it here first, and if there are no issues, I'll submit the PR for the documentation.
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: #15238
Currently, NestJS WebSocket gateways only support static path matching. Dynamic path parameters (like
/chat/:roomId/socket
) are not supported, making it difficult to create RESTful WebSocket endpoints with path-based routing.What is the new behavior?
This PR introduces WebSocket Wildcard URL Support with dynamic path parameter extraction, bringing WebSocket gateways on par with HTTP controllers in terms of routing flexibility.
Key Features Added:
1. Dynamic Path Parameter Support
/chat/:roomId/socket
,/game/:gameId/room/:roomId/socket
:param
syntaxpath-to-regexp
patterns used throughout NestJS2. New
@WsParam()
Decorator3. Performance Optimized Architecture
4. Full TypeScript Integration
@WsParam('userId', ParseIntPipe) userId: number
Changes Summary:
[email protected]
(consistent with NestJS HTTP routing)Technical Implementation:
WsAdapter
with intelligent path compilation and matchingWsParamtype
enum andWsParamsFactory
for parameter extractionThis implementation addresses the exact requirements from Issue #15238 and provides developers with the flexibility to build sophisticated real-time applications with clean, RESTful WebSocket endpoints!
Does this PR introduce a breaking change?
No breaking changes - All existing WebSocket gateways continue to work exactly as before. This is purely additive functionality that developers can opt into when needed.
Other information
This feature has been thoroughly tested and aligns perfectly with NestJS's design philosophy! 🎉