Skip to content

Commit 2413873

Browse files
authored
Merge pull request #14 from sparrowapp-dev/feat/implement-socket-io-and-websocket-proxy
feat: implement socket io and websocket proxy
2 parents 5a2ecb0 + 9cb2f2c commit 2413873

File tree

6 files changed

+180
-200
lines changed

6 files changed

+180
-200
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"eslint-plugin-prettier": "^5.0.0",
5151
"jest": "^29.5.0",
5252
"prettier": "^3.0.0",
53+
"@types/ws": "^8.5.13",
5354
"source-map-support": "^0.5.21",
5455
"supertest": "^6.3.3",
5556
"ts-jest": "^29.1.0",

src/main.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { NestFactory } from '@nestjs/core';
22
import { AppModule } from './app.module';
3-
import { WsAdapter } from '@nestjs/platform-ws';
43
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
54
import * as bodyParser from 'body-parser';
5+
import { IoAdapter } from '@nestjs/platform-socket.io';
66

77
async function bootstrap() {
88
const app = await NestFactory.create(AppModule);
9-
app.useWebSocketAdapter(new WsAdapter(app));
10-
119
// Increase payload size limit
1210
app.use(bodyParser.json({ limit: '50mb' }));
1311
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
@@ -33,6 +31,9 @@ async function bootstrap() {
3331
*/
3432
const SWAGGER_API_CURRENT_VERSION = '1.0';
3533

34+
// Use the custom WebSocket adapter to handle both WS and SocketIo
35+
app.useWebSocketAdapter(new IoAdapter(app));
36+
3637
// Configure Swagger options for API documentation
3738
const options = new DocumentBuilder()
3839
.setTitle(SWAGGER_API_NAME)

src/proxy/socketio/socketio.gateway.ts

Lines changed: 95 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,91 +2,121 @@ import {
22
WebSocketGateway,
33
WebSocketServer,
44
OnGatewayConnection,
5-
OnGatewayDisconnect,
6-
} from '@nestjs/websockets';
7-
import { Server, Socket } from 'socket.io';
8-
import { SocketIoService } from './socketio.service';
5+
} from "@nestjs/websockets";
6+
import { Server, Socket as SocketServer } from "socket.io";
7+
import { SocketIoService } from "./socketio.service";
8+
import { WebSocketService } from "./websocket.service";
99

10-
@WebSocketGateway({ cors: true, path: '/' })
11-
export class SocketIoGateway
12-
implements OnGatewayConnection, OnGatewayDisconnect
13-
{
10+
@WebSocketGateway( {
11+
cors: {
12+
origin: "*",
13+
methods: ["GET", "POST"],
14+
},
15+
path: "/socket.io",
16+
transports: ["websocket"],
17+
})
18+
export class SocketIoGateway implements OnGatewayConnection {
1419
@WebSocketServer()
1520
server: Server;
1621

17-
constructor(private readonly socketIoService: SocketIoService) {}
22+
constructor(
23+
private readonly socketIoService: SocketIoService,
24+
private readonly websocketService: WebSocketService,
25+
) {}
1826

19-
/**
20-
* Handle WebSocket connection from the frontend with `tabid`.
21-
*/
22-
async handleConnection(client: Socket) {
23-
const { url, namespace, tabid, headers } = client.handshake.query;
27+
async afterInit() {
28+
console.log("Socket.io Handler Gateway initialized!");
29+
}
30+
31+
async handleConnection(proxySocketIO: SocketServer) {
32+
const { targetUrl, namespace, headers, targetType } =
33+
proxySocketIO.handshake.query;
2434

25-
if (!url || !namespace || !tabid) {
26-
client.disconnect();
35+
if (!targetUrl || !namespace) {
36+
proxySocketIO.disconnect();
2737
console.error(
28-
'Missing required query parameters: url, namespace, or tabid',
38+
"Missing required query parameters: url, namespace, or tabid",
2939
);
3040
return;
3141
}
3242

3343
try {
34-
const parsedHeaders = headers ? JSON.parse(headers as string) : {};
35-
36-
// Register frontend client
37-
this.socketIoService.registerFrontendClient(tabid as string, client);
38-
39-
// Connect to the real Socket.IO server
40-
await this.socketIoService.connectToRealSocket(
41-
tabid as string,
42-
url as string,
43-
namespace as string,
44-
parsedHeaders,
44+
const parsedHeaders = JSON.parse(headers as string);
45+
const headersObject: { [key: string]: string } = parsedHeaders.reduce(
46+
(
47+
acc: Record<string, string>,
48+
{ key, value }: { key: string; value: string },
49+
) => {
50+
acc[key] = value;
51+
return acc;
52+
},
53+
{} as { [key: string]: string },
4554
);
4655

47-
console.log(`Proxy connection established for TabID=${tabid}`);
56+
if (targetType === "socketio") {
57+
// Establish a connection to the real Socket.IO server
58+
const targetSocketIO =
59+
await this.socketIoService.connectToTargetSocketIO(
60+
proxySocketIO,
61+
targetUrl as string,
62+
namespace as string,
63+
headersObject,
64+
);
65+
66+
proxySocketIO.on("disconnect", async () => {
67+
// Disconnecting target Socket.IO will automatically disconnects proxy Socket.IO in chain.
68+
targetSocketIO?.disconnect();
69+
});
4870

49-
// Listen for all dynamic events from the frontend and forward them
50-
client.onAny(async (event: string, ...args: any[]) => {
51-
console.log(
52-
`Received event from frontend for TabID=${tabid}: ${event}`,
53-
args,
71+
// Listen for all dynamic events from the frontend and forward them to target Socket.IO.
72+
proxySocketIO.onAny(async (event: string, args: any) => {
73+
try {
74+
if (event === "sparrow_internal_disconnect") {
75+
// Disconnecting target Socket.IO will automatically disconnects proxy Socket.IO in chain.
76+
targetSocketIO?.disconnect();
77+
} else {
78+
targetSocketIO?.emit(event, args);
79+
}
80+
} catch (err) {
81+
console.error(
82+
`Failed to forward event ${event} for ${err.message}`,
83+
);
84+
}
85+
});
86+
} else if (targetType === "websocket") {
87+
// Establish a connection to the real Socket.IO server
88+
const targetWebSocket = await this.websocketService.establishConnection(
89+
proxySocketIO,
90+
targetUrl as string,
91+
headersObject,
5492
);
5593

56-
try {
57-
// Forward the dynamic event and its arguments to the real server
58-
await this.socketIoService.emitToRealSocket(
59-
tabid as string,
60-
event,
61-
args,
62-
);
63-
} catch (err) {
64-
console.error(
65-
`Failed to forward event ${event} for TabID=${tabid}: ${err.message}`,
66-
);
67-
}
68-
});
94+
proxySocketIO.on("disconnect", async () => {
95+
// Disconnecting target Socket.IO will automatically disconnects proxy Socket.IO in chain.
96+
targetWebSocket?.close();
97+
});
98+
99+
// Listen for all dynamic events from the frontend and forward them to target Socket.IO.
100+
proxySocketIO.onAny(async (event: string, args: any) => {
101+
try {
102+
if (event === "sparrow_internal_disconnect") {
103+
// Disconnecting target Socket.IO will automatically disconnects proxy Socket.IO in chain.
104+
targetWebSocket?.close();
105+
} else {
106+
targetWebSocket?.send(args);
107+
}
108+
} catch (err) {
109+
console.error(
110+
`Failed to forward event ${event} for ${err.message}`,
111+
);
112+
}
113+
});
114+
}
69115
} catch (err) {
70116
console.error(
71-
`Failed to connect to real Socket.IO server for TabID=${tabid}: ${err.message}`,
117+
`Failed to connect to real Socket.IO server for ${err.message}`,
72118
);
73-
client.disconnect();
119+
proxySocketIO.disconnect();
74120
}
75121
}
76-
77-
/**
78-
* Handle WebSocket disconnection for a specific `tabid`.
79-
*/
80-
async handleDisconnect(client: Socket) {
81-
const { tabid } = client.handshake.query;
82-
83-
if (!tabid) {
84-
console.error('TabID missing on disconnection');
85-
return;
86-
}
87-
88-
console.log(`Proxy connection closed for TabID=${tabid}`);
89-
await this.socketIoService.disconnectFromRealSocket(tabid as string);
90-
this.socketIoService.unregisterFrontendClient(tabid as string);
91-
}
92122
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Module } from '@nestjs/common';
22
import { SocketIoGateway } from './socketio.gateway';
33
import { SocketIoService } from './socketio.service';
4+
import { WebSocketService } from './websocket.service';
45

56
@Module({
6-
providers: [SocketIoGateway, SocketIoService],
7-
exports: [SocketIoService],
7+
providers: [SocketIoGateway, SocketIoService, WebSocketService],
8+
exports: [SocketIoService, WebSocketService],
89
})
910
export class SocketIoModule {}

0 commit comments

Comments
 (0)