redweb 0.6.4 → 0.6.6
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 +63 -85
- package/index.d.ts +85 -35
- package/index.js +9 -4
- package/package.json +1 -1
- package/src/ws/BaseHandler.js +14 -73
- package/src/ws/BaseSocketServer.js +17 -163
- package/src/ws/DefaultHandler.js +14 -0
- package/src/ws/DefaultRoute.js +14 -0
- package/src/ws/SocketRoute.js +114 -0
- package/src/ws/index.js +1 -0
- package/src/ws/util.js +9 -0
package/README.md
CHANGED
|
@@ -74,9 +74,9 @@ const options = {
|
|
|
74
74
|
const app = new HttpsServer(options);
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
### WebSocket Server with Handlers
|
|
77
|
+
### WebSocket Server with Routes and Handlers
|
|
78
78
|
|
|
79
|
-
RedWeb
|
|
79
|
+
RedWeb uses **route-based architecture** for WebSocket connections, allowing you to modularize and secure your WebSocket message handling logic.
|
|
80
80
|
|
|
81
81
|
#### Defining a Custom Handler
|
|
82
82
|
|
|
@@ -87,86 +87,85 @@ const { BaseHandler } = require('redweb');
|
|
|
87
87
|
|
|
88
88
|
class ChatHandler extends BaseHandler {
|
|
89
89
|
constructor() {
|
|
90
|
-
super(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
});
|
|
90
|
+
super('chat');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onMessage(socket, message) {
|
|
94
|
+
console.log(`Received chat message: ${message.text}`);
|
|
95
|
+
socket.send(JSON.stringify({ type: 'chatResponse', message: 'Hello!' }));
|
|
99
96
|
}
|
|
100
97
|
}
|
|
101
98
|
|
|
102
99
|
module.exports = ChatHandler;
|
|
103
100
|
```
|
|
104
101
|
|
|
105
|
-
####
|
|
102
|
+
#### Defining a WebSocket Route
|
|
103
|
+
|
|
104
|
+
Routes group handlers and specify the WebSocket path.
|
|
106
105
|
|
|
107
106
|
```javascript
|
|
108
|
-
const {
|
|
107
|
+
const { SocketRoute } = require('redweb');
|
|
109
108
|
const ChatHandler = require('./ChatHandler');
|
|
110
109
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
connectionCloseCallback: (socket) => {
|
|
118
|
-
console.log('WebSocket client disconnected');
|
|
110
|
+
class ChatRoute extends SocketRoute {
|
|
111
|
+
constructor() {
|
|
112
|
+
super({
|
|
113
|
+
path: '/chat',
|
|
114
|
+
handlers: [ChatHandler]
|
|
115
|
+
});
|
|
119
116
|
}
|
|
120
|
-
}
|
|
117
|
+
}
|
|
121
118
|
|
|
122
|
-
|
|
119
|
+
module.exports = ChatRoute;
|
|
123
120
|
```
|
|
124
121
|
|
|
125
|
-
####
|
|
122
|
+
#### Setting Up a WebSocket Server with Routes
|
|
126
123
|
|
|
127
|
-
|
|
124
|
+
```javascript
|
|
125
|
+
const { SocketServer } = require('redweb');
|
|
126
|
+
const ChatRoute = require('./ChatRoute');
|
|
127
|
+
|
|
128
|
+
new SocketServer({
|
|
129
|
+
port: 3000,
|
|
130
|
+
routes: [ChatRoute]
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Adding Routes Dynamically
|
|
135
|
+
|
|
136
|
+
Routes can be added to the WebSocket server after initialization.
|
|
128
137
|
|
|
129
138
|
```javascript
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this.name = 'DynamicHandler';
|
|
133
|
-
this.handlers = {
|
|
134
|
-
dynamicMessage: (socket, data) => {
|
|
135
|
-
console.log(`Received dynamic message: ${data.message}`);
|
|
136
|
-
socket.send(JSON.stringify({ type: 'dynamicResponse', message: 'Handled dynamically!' }));
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
}
|
|
139
|
+
const { SocketServer, SocketRoute } = require('redweb');
|
|
140
|
+
const ChatHandler = require('./ChatHandler');
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
class ChatRoute extends SocketRoute {
|
|
143
|
+
constructor() {
|
|
144
|
+
super({
|
|
145
|
+
path: '/chat',
|
|
146
|
+
handlers: [ChatHandler]
|
|
147
|
+
});
|
|
143
148
|
}
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
const { SocketServer } = require('redweb');
|
|
147
151
|
const socketServer = new SocketServer({ port: 3000 });
|
|
148
152
|
|
|
149
|
-
// Dynamically add a new
|
|
150
|
-
|
|
153
|
+
// Dynamically add a new route
|
|
154
|
+
const chatRoute = new ChatRoute();
|
|
155
|
+
socketServer.routes.push(chatRoute);
|
|
151
156
|
```
|
|
152
157
|
|
|
153
|
-
|
|
158
|
+
### Client Communication with a Route
|
|
154
159
|
|
|
155
|
-
The client
|
|
160
|
+
The client connects to the WebSocket server using the specified route.
|
|
156
161
|
|
|
157
162
|
```javascript
|
|
158
163
|
const WebSocket = require('ws');
|
|
159
164
|
|
|
160
|
-
const ws = new WebSocket('ws://localhost:3000');
|
|
165
|
+
const ws = new WebSocket('ws://localhost:3000/chat');
|
|
161
166
|
|
|
162
167
|
ws.on('open', () => {
|
|
163
|
-
ws.send(JSON.stringify({
|
|
164
|
-
type: '__handlerConnect',
|
|
165
|
-
data: { handlerName: 'ChatHandler' }
|
|
166
|
-
}));
|
|
167
|
-
|
|
168
|
-
// Send a message to the handler
|
|
169
|
-
ws.send(JSON.stringify({ type: 'chat', message: 'Hi there!' }));
|
|
168
|
+
ws.send(JSON.stringify({ type: 'chat', text: 'Hello there!' }));
|
|
170
169
|
});
|
|
171
170
|
|
|
172
171
|
ws.on('message', (message) => {
|
|
@@ -176,42 +175,25 @@ ws.on('message', (message) => {
|
|
|
176
175
|
|
|
177
176
|
### Managing Connected Clients
|
|
178
177
|
|
|
179
|
-
RedWeb's WebSocket server maintains a list of connected clients by their IP addresses. This list is automatically updated when clients connect or disconnect.
|
|
178
|
+
RedWeb's WebSocket server maintains a list of connected clients by their IP addresses for each route. This list is automatically updated when clients connect or disconnect.
|
|
180
179
|
|
|
181
180
|
```javascript
|
|
182
|
-
const {
|
|
181
|
+
const { SocketRoute } = require('redweb');
|
|
183
182
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
console.log('WebSocket client disconnected');
|
|
183
|
+
class ChatRoute extends SocketRoute {
|
|
184
|
+
constructor() {
|
|
185
|
+
super({
|
|
186
|
+
path: '/chat',
|
|
187
|
+
handlers: []
|
|
188
|
+
});
|
|
191
189
|
}
|
|
192
|
-
});
|
|
193
190
|
|
|
194
|
-
|
|
195
|
-
console.log(
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### Backward-Compatible Message Handlers
|
|
199
|
-
|
|
200
|
-
If no handler is assigned during the initial connection, you can still use traditional `messageHandlers`.
|
|
201
|
-
|
|
202
|
-
```javascript
|
|
203
|
-
const { SocketServer } = require('redweb');
|
|
204
|
-
|
|
205
|
-
const options = {
|
|
206
|
-
port: 3000,
|
|
207
|
-
messageHandlers: {
|
|
208
|
-
echo: (socket, data) => {
|
|
209
|
-
socket.send(JSON.stringify({ type: 'echoResponse', message: data.message }));
|
|
210
|
-
}
|
|
191
|
+
onConnection(socket) {
|
|
192
|
+
console.log('New client connected:', socket.remoteAddress);
|
|
211
193
|
}
|
|
212
|
-
}
|
|
194
|
+
}
|
|
213
195
|
|
|
214
|
-
|
|
196
|
+
module.exports = ChatRoute;
|
|
215
197
|
```
|
|
216
198
|
|
|
217
199
|
## Options
|
|
@@ -226,14 +208,10 @@ const socketServer = new SocketServer(options);
|
|
|
226
208
|
- **encoding**: Encoding type for request bodies (`'json'` or `'urlencoded'`).
|
|
227
209
|
- **ssl**: SSL configuration for HTTPS server (`{ key: './path/to/key.pem', cert: './path/to/cert.pem' }`).
|
|
228
210
|
|
|
229
|
-
### SocketServer
|
|
211
|
+
### SocketServer Options
|
|
230
212
|
|
|
231
213
|
- **port**: Port number (default: `3000`).
|
|
232
|
-
- **
|
|
233
|
-
- **connectionCloseCallback**: Function to execute once a client disconnects.
|
|
234
|
-
- **messageCallback**: Function to execute for every message received.
|
|
235
|
-
- **messageHandlers**: Object containing message handlers based on message type.
|
|
236
|
-
- **handlerConfig**: Array of handler classes to support handler-based routing.
|
|
214
|
+
- **routes**: Array of `SocketRoute` classes to define WebSocket routes and handlers.
|
|
237
215
|
- **ssl**: SSL configuration for SecureSocketServer (`{ key: './path/to/key.pem', cert: './path/to/cert.pem' }`).
|
|
238
216
|
|
|
239
217
|
## License
|
package/index.d.ts
CHANGED
|
@@ -59,61 +59,103 @@ declare module 'redweb' {
|
|
|
59
59
|
key: string;
|
|
60
60
|
cert: string;
|
|
61
61
|
};
|
|
62
|
-
|
|
62
|
+
routes?: Array<new () => SocketRoute>;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
66
|
+
* WebSocket route configuration.
|
|
67
67
|
*/
|
|
68
|
-
export
|
|
68
|
+
export interface SocketRouteConfig {
|
|
69
|
+
path: string; // The WebSocket route path (e.g., "/chat").
|
|
70
|
+
handlers: Array<new () => BaseHandler>; // Array of handler classes for the route.
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Represents a WebSocket route.
|
|
75
|
+
*/
|
|
76
|
+
export class SocketRoute {
|
|
69
77
|
/**
|
|
70
|
-
* The
|
|
78
|
+
* The path for the WebSocket route.
|
|
71
79
|
*/
|
|
72
|
-
|
|
80
|
+
path: string;
|
|
73
81
|
|
|
74
82
|
/**
|
|
75
|
-
*
|
|
83
|
+
* Handlers associated with the route.
|
|
76
84
|
*/
|
|
77
|
-
|
|
78
|
-
[type: string]: (socket: WebSocket, data: any) => void;
|
|
79
|
-
};
|
|
85
|
+
handlers: BaseHandler[];
|
|
80
86
|
|
|
81
87
|
/**
|
|
82
|
-
*
|
|
88
|
+
* Creates a new `SocketRoute` instance.
|
|
89
|
+
* @param config - Configuration options for the route.
|
|
83
90
|
*/
|
|
84
|
-
|
|
91
|
+
constructor(config: SocketRouteConfig);
|
|
85
92
|
|
|
86
93
|
/**
|
|
87
|
-
*
|
|
88
|
-
* @param
|
|
94
|
+
* Adds a new handler dynamically.
|
|
95
|
+
* @param HandlerClass - A class extending `BaseHandler`.
|
|
89
96
|
*/
|
|
90
|
-
|
|
97
|
+
addHandler(HandlerClass: new () => BaseHandler): void;
|
|
91
98
|
|
|
92
99
|
/**
|
|
93
|
-
*
|
|
94
|
-
* @param socket - The WebSocket connection
|
|
95
|
-
* @param
|
|
100
|
+
* Handles a new WebSocket connection.
|
|
101
|
+
* @param socket - The WebSocket connection instance.
|
|
102
|
+
* @param req - The HTTP request associated with the connection.
|
|
96
103
|
*/
|
|
97
|
-
|
|
104
|
+
handleConnection(socket: WebSocket, req: import('http').IncomingMessage): void;
|
|
98
105
|
|
|
99
106
|
/**
|
|
100
|
-
*
|
|
101
|
-
* @param socket - The WebSocket connection
|
|
107
|
+
* Handles incoming WebSocket messages.
|
|
108
|
+
* @param socket - The WebSocket connection instance.
|
|
109
|
+
* @param data - The message data.
|
|
102
110
|
*/
|
|
103
|
-
|
|
111
|
+
handleMessage(socket: WebSocket, data: any): void;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Handles WebSocket disconnections.
|
|
115
|
+
* @param socket - The WebSocket connection instance.
|
|
116
|
+
*/
|
|
117
|
+
handleClose(socket: WebSocket): void;
|
|
104
118
|
|
|
105
119
|
/**
|
|
106
|
-
* Handles
|
|
120
|
+
* Handles WebSocket errors.
|
|
121
|
+
* @param socket - The WebSocket connection instance.
|
|
122
|
+
* @param error - The error object.
|
|
123
|
+
*/
|
|
124
|
+
handleError(socket: WebSocket, error: Error): void;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Base class for WebSocket handlers.
|
|
129
|
+
*/
|
|
130
|
+
export class BaseHandler {
|
|
131
|
+
/**
|
|
132
|
+
* The name of the handler (used to identify it in the server).
|
|
133
|
+
*/
|
|
134
|
+
name: string;
|
|
135
|
+
/**
|
|
136
|
+
* Creates a new handler instance.
|
|
137
|
+
* @param name - The name of the handler.
|
|
138
|
+
*/
|
|
139
|
+
constructor(name: string);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Handles an incoming message.
|
|
107
143
|
* @param socket - The WebSocket connection that sent the message.
|
|
108
|
-
* @param message - The
|
|
144
|
+
* @param message - The message data.
|
|
109
145
|
*/
|
|
110
|
-
|
|
146
|
+
onMessage(socket: WebSocket, message: Object): void;
|
|
111
147
|
|
|
112
148
|
/**
|
|
113
|
-
*
|
|
114
|
-
* @param
|
|
149
|
+
* Called during the first contact with a new WebSocket connection.
|
|
150
|
+
* @param socket - The WebSocket connection instance.
|
|
115
151
|
*/
|
|
116
|
-
|
|
152
|
+
onInitialContact(socket: WebSocket): void;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Called when a WebSocket connection closes.
|
|
156
|
+
* @param socket - The WebSocket connection instance.
|
|
157
|
+
*/
|
|
158
|
+
onClose(socket: WebSocket): void;
|
|
117
159
|
}
|
|
118
160
|
|
|
119
161
|
/**
|
|
@@ -121,22 +163,30 @@ declare module 'redweb' {
|
|
|
121
163
|
*/
|
|
122
164
|
export class BaseSocketServer {
|
|
123
165
|
/**
|
|
124
|
-
*
|
|
166
|
+
* List of WebSocket routes.
|
|
125
167
|
*/
|
|
126
|
-
|
|
168
|
+
routes: SocketRoute[];
|
|
127
169
|
|
|
128
170
|
/**
|
|
129
|
-
*
|
|
171
|
+
* Creates a new `BaseSocketServer`.
|
|
172
|
+
* @param server - The HTTP server instance.
|
|
173
|
+
* @param options - Configuration options.
|
|
130
174
|
*/
|
|
131
|
-
handlers: BaseHandler[];
|
|
132
|
-
|
|
133
175
|
constructor(server: HTTPServer | HTTPSServer, options?: SocketServerOptions);
|
|
134
176
|
|
|
135
177
|
/**
|
|
136
|
-
*
|
|
137
|
-
* @param
|
|
178
|
+
* Handles WebSocket upgrade requests.
|
|
179
|
+
* @param req - The incoming HTTP upgrade request.
|
|
180
|
+
* @param socket - The raw network socket.
|
|
181
|
+
* @param head - The initial data chunk.
|
|
138
182
|
*/
|
|
139
|
-
|
|
183
|
+
handleUpgrade(req: import('http').IncomingMessage, socket: import('net').Socket, head: Buffer): void;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
*
|
|
187
|
+
* @param route
|
|
188
|
+
*/
|
|
189
|
+
addRoute(route: new () => SocketRoute);
|
|
140
190
|
}
|
|
141
191
|
|
|
142
192
|
/**
|
package/index.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const {
|
|
1
|
+
const { METHODS } = require('./src/http');
|
|
2
|
+
const { sendJson } = require('./src/ws/util');
|
|
3
|
+
const { SocketServer, SecureSocketServer, SOCKET_OPTIONS, SocketRoute } = require('./src/ws');
|
|
3
4
|
const { BaseHandler } = require('./src/ws/BaseHandler');
|
|
5
|
+
const HttpServer = require('./src/http/HttpServer');
|
|
6
|
+
const HttpsServer = require('./src/http/HttpsServer');
|
|
4
7
|
module.exports = {
|
|
8
|
+
HttpServer,
|
|
9
|
+
HttpsServer,
|
|
5
10
|
SocketServer,
|
|
6
11
|
SecureSocketServer,
|
|
7
12
|
BaseHandler,
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
SocketRoute,
|
|
14
|
+
sendJson,
|
|
10
15
|
SOCKET_OPTIONS,
|
|
11
16
|
METHODS
|
|
12
17
|
};
|
package/package.json
CHANGED
package/src/ws/BaseHandler.js
CHANGED
|
@@ -4,94 +4,35 @@
|
|
|
4
4
|
class BaseHandler {
|
|
5
5
|
/**
|
|
6
6
|
* Creates a new handler instance.
|
|
7
|
-
* @param {
|
|
8
|
-
* - `name`: The unique name of the handler.
|
|
9
|
-
* - `handlers`: A dictionary of message types and their corresponding handler functions.
|
|
7
|
+
* @param {string} name - The name of the Handler, used in the client 'type' arg of request e.g {"type": "<handler-name>", ...}
|
|
10
8
|
*/
|
|
11
|
-
constructor(
|
|
9
|
+
constructor(name) {
|
|
12
10
|
/**
|
|
13
|
-
*
|
|
11
|
+
* he name of the Handler, used in the client 'type' arg of request e.g {"type": "<handler-name>", ...}.
|
|
14
12
|
* @type {string}
|
|
15
13
|
*/
|
|
16
|
-
this.name =
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The dictionary of message handlers for this handler.
|
|
20
|
-
* @type {Record<string, (socket: WebSocket, data: object) => void>}
|
|
21
|
-
*/
|
|
22
|
-
this.messageHandlers = config.handlers || {};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The list of active WebSocket connections managed by this handler.
|
|
26
|
-
* @type {WebSocket[]}
|
|
27
|
-
*/
|
|
28
|
-
this.connections = [];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
handleSocketMessages(socket) {
|
|
32
|
-
socket.on('message', (message) => this.handleMessage(socket, message));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
handleSocketClose(socket) {
|
|
36
|
-
socket.on('close', () => {
|
|
37
|
-
this.onClose(socket);
|
|
38
|
-
this.connections = this.connections.filter((conn) => conn !== socket);
|
|
39
|
-
console.log(`Connection closed for handler "${this.name}".`);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
handleNewConnection(socket) {
|
|
44
|
-
this.connections.push(socket);
|
|
45
|
-
this.handleSocketMessages(socket);
|
|
46
|
-
this.handleSocketClose(socket);
|
|
47
|
-
socket.isAssigned = true;
|
|
48
|
-
console.log(`New connection added to handler "${this.name}".`);
|
|
14
|
+
this.name = name;
|
|
49
15
|
}
|
|
50
|
-
|
|
51
16
|
/**
|
|
52
|
-
*
|
|
53
|
-
* @param {WebSocket} socket - The WebSocket connection
|
|
17
|
+
* Handles an incoming message and routes it to the appropriate handler function.
|
|
18
|
+
* @param {WebSocket & {sendJson: (message: Object) => void, broadcast: (message: Object) => void}} socket - The WebSocket connection that sent the message.
|
|
19
|
+
* @param {string} message - The incoming message in JSON string format.
|
|
54
20
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.handleNewConnection(socket);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
onClose(socket) {
|
|
61
|
-
console.log(`Closing socket ${socket.remoteAddress}`);
|
|
21
|
+
handleMessage(socket, message) {
|
|
22
|
+
this.onMessage(socket, message);
|
|
62
23
|
}
|
|
63
24
|
|
|
64
25
|
/**
|
|
65
|
-
*
|
|
26
|
+
* Method to be overriden to process messages.
|
|
66
27
|
* @param {WebSocket} socket - The WebSocket connection that sent the message.
|
|
67
28
|
* @param {string} message - The incoming message in JSON string format.
|
|
68
29
|
*/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const parsedMessage = JSON.parse(message);
|
|
72
|
-
const { type, ...data } = parsedMessage;
|
|
73
|
-
|
|
74
|
-
if (this.messageHandlers[type]) {
|
|
75
|
-
this.messageHandlers[type](socket, data);
|
|
76
|
-
} else {
|
|
77
|
-
console.warn(`Unhandled message type: "${type}" in handler "${this.name}".`);
|
|
78
|
-
socket.close();
|
|
79
|
-
}
|
|
80
|
-
} catch (error) {
|
|
81
|
-
console.error(`Error processing message in handler "${this.name}":`, error);
|
|
82
|
-
}
|
|
30
|
+
onMessage(socket, message) {
|
|
31
|
+
throw "Not yet implemented!";
|
|
83
32
|
}
|
|
84
33
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
* @param {object} message - The message to broadcast.
|
|
88
|
-
*/
|
|
89
|
-
broadcast(message) {
|
|
90
|
-
this.connections.forEach((socket) => {
|
|
91
|
-
if (socket.readyState === WebSocket.OPEN) {
|
|
92
|
-
socket.send(JSON.stringify(message));
|
|
93
|
-
}
|
|
94
|
-
});
|
|
34
|
+
onInitialContact(socket) {
|
|
35
|
+
|
|
95
36
|
}
|
|
96
37
|
}
|
|
97
38
|
|
|
@@ -1,26 +1,16 @@
|
|
|
1
|
-
const WebSocket = require('ws');
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @typedef {Object} SocketServerOptions
|
|
5
3
|
* @property {import('http').Server} [server] - The HTTP server instance to bind the WebSocket server to.
|
|
6
4
|
* @property {number} [port=3000] - The port number for the WebSocket server.
|
|
7
|
-
* @property {(
|
|
8
|
-
* @property {(socket: WebSocket) => void} [connectionCloseCallback] - Callback executed when a client disconnects.
|
|
9
|
-
* @property {(socket: WebSocket, message: object) => void} [messageCallback] - Callback executed when a message is received.
|
|
10
|
-
* @property {Record<string, (socket: WebSocket, data: object) => void>} [messageHandlers] - A dictionary of message types and their corresponding handler functions.
|
|
11
|
-
* @property {{ key: string, cert: string } | null} [ssl] - SSL configuration for a secure WebSocket server.
|
|
12
|
-
* @property {Array<new () => BaseHandler>} [handlerConfig] - An array of handler classes to use for routing.
|
|
5
|
+
* @property {Array<new () => import('./SocketRoute').SocketRoute>} [routes] - An array of handler classes to use for routing.
|
|
13
6
|
*/
|
|
14
7
|
|
|
8
|
+
const DefaultRoute = require('./DefaultRoute');
|
|
9
|
+
|
|
15
10
|
const SOCKET_OPTIONS = {
|
|
16
11
|
port: 3000,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
messageCallback: undefined,
|
|
20
|
-
messageHandlers: {
|
|
21
|
-
ping: (socket, data) => socket.send(JSON.stringify({ type: 'pong' }))
|
|
22
|
-
},
|
|
23
|
-
ssl: null
|
|
12
|
+
ssl: null,
|
|
13
|
+
routes: []
|
|
24
14
|
};
|
|
25
15
|
|
|
26
16
|
/**
|
|
@@ -41,162 +31,26 @@ class BaseSocketServer {
|
|
|
41
31
|
* @param {SocketServerOptions} options - The configuration options for the WebSocket server.
|
|
42
32
|
*/
|
|
43
33
|
constructor(server, options = {}) {
|
|
44
|
-
this.wss = new WebSocket.Server({ server });
|
|
45
34
|
this.clients = new Map(); // Map of clients by their IP addresses.
|
|
46
35
|
Object.assign(this, { ...SOCKET_OPTIONS, ...options });
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Initializes the handler classes.
|
|
54
|
-
* @param {Array<new () => BaseHandler>} handlerConfig - Array of handler classes.
|
|
55
|
-
* @returns {BaseHandler[]} - Array of handler instances.
|
|
56
|
-
*/
|
|
57
|
-
initHandlers(handlerConfig) {
|
|
58
|
-
return handlerConfig.map(HandlerClass => new HandlerClass());
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Adds a new handler to the WebSocket server.
|
|
63
|
-
* @param {new () => BaseHandler} HandlerClass - The handler class to add.
|
|
64
|
-
*/
|
|
65
|
-
addHandler(HandlerClass) {
|
|
66
|
-
const newHandler = new HandlerClass();
|
|
67
|
-
if (this.handlers.find(handler => handler.name === newHandler.name)) {
|
|
68
|
-
console.warn(`Handler with name '${newHandler.name}' already exists.`);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
this.handlers.push(newHandler);
|
|
72
|
-
console.log(`Handler '${newHandler.name}' added successfully.`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Handles a new WebSocket connection.
|
|
77
|
-
* @param {WebSocket} socket - The WebSocket connection instance.
|
|
78
|
-
* @param {import('http').IncomingMessage} req - The HTTP request object associated with the connection.
|
|
79
|
-
*/
|
|
80
|
-
handleConnection(socket, req) {
|
|
81
|
-
const ip = req.socket.remoteAddress;
|
|
82
|
-
console.log(`New client connected: ${ip}`);
|
|
83
|
-
if (this.clients.get(ip) !== undefined) {
|
|
84
|
-
const oldClient = this.clients.get(ip);
|
|
85
|
-
console.warn(`Client ${ip} already connected, disconnecting existing connection.`);
|
|
86
|
-
oldClient.send(
|
|
87
|
-
JSON.stringify({ msg: 'You are being disconnected because a new client is connected with your IP address.' })
|
|
88
|
-
);
|
|
89
|
-
oldClient.close();
|
|
90
|
-
}
|
|
91
|
-
this.clients.set(ip, socket);
|
|
92
|
-
socket.isAssigned = false; // Tracks whether the socket has been assigned a handler.
|
|
93
|
-
|
|
94
|
-
if (this.connectionOpenCallback) this.connectionOpenCallback(socket);
|
|
95
|
-
|
|
96
|
-
socket.on('message', message => this.initialMessageHandler(socket, message, ip));
|
|
97
|
-
socket.on('close', () => this.handleClose(socket, ip));
|
|
98
|
-
socket.on('error', error => this.handleError(socket, error, ip));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Processes the initial message to determine if a handler assignment is required.
|
|
103
|
-
* @param {WebSocket} socket - The WebSocket connection instance.
|
|
104
|
-
* @param {string} message - The received message.
|
|
105
|
-
* @param {string} ip - The client's IP address.
|
|
106
|
-
*/
|
|
107
|
-
initialMessageHandler(socket, message, ip) {
|
|
108
|
-
try {
|
|
109
|
-
const parsedMessage = JSON.parse(message);
|
|
110
|
-
|
|
111
|
-
// If the first message is '__handlerConnect', attempt to assign a handler
|
|
112
|
-
if (parsedMessage.type === '__handlerConnect') {
|
|
113
|
-
this.assignToHandler(socket, parsedMessage);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// If no handler is assigned, use the default messageHandlers or fallback
|
|
118
|
-
if (!socket.isAssigned) {
|
|
119
|
-
this.handleMessage(socket, parsedMessage, ip);
|
|
120
|
-
} else {
|
|
121
|
-
throw new Error('Unexpected message after handler assignment');
|
|
122
|
-
}
|
|
123
|
-
} catch (error) {
|
|
124
|
-
console.error(`Error handling initial message from ${ip}:`, error);
|
|
125
|
-
socket.close(); // Optionally close the socket for invalid behavior.
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Assigns the socket to a specific handler.
|
|
131
|
-
* @param {WebSocket} socket - The WebSocket connection instance.
|
|
132
|
-
* @param {{ handlerName: string, key?: string }} data - Data containing the handler name and optional authentication key.
|
|
133
|
-
*/
|
|
134
|
-
assignToHandler(socket, connectMessage) {
|
|
135
|
-
if (!connectMessage.data) {
|
|
136
|
-
console.warn(`No data provided for handlerConnect message.`);
|
|
137
|
-
socket.close();
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const { handlerName, data } = connectMessage.data;
|
|
141
|
-
const handler = this.handlers.find(h => h.name === handlerName);
|
|
142
|
-
if (!handler) {
|
|
143
|
-
console.warn(`Handler not found: ${handlerName}`);
|
|
144
|
-
socket.send(JSON.stringify({ error: `Handler '${handlerName}' not found` }));
|
|
145
|
-
socket.close();
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Optional: Perform authentication with `key`
|
|
150
|
-
socket.removeAllListeners('message');
|
|
151
|
-
handler.newConnection(socket, connectMessage.data);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Handles a message using the default messageHandlers.
|
|
156
|
-
* @param {WebSocket} socket - The WebSocket connection instance.
|
|
157
|
-
* @param {{ type: string, [key: string]: any }} parsedMessage - The parsed message object.
|
|
158
|
-
* @param {string} ip - The client's IP address.
|
|
159
|
-
*/
|
|
160
|
-
handleMessage(socket, parsedMessage, ip) {
|
|
161
|
-
const { type, ...data } = parsedMessage;
|
|
162
|
-
|
|
163
|
-
if (this.messageCallback) this.messageCallback(socket, parsedMessage);
|
|
164
|
-
|
|
165
|
-
if (this.messageHandlers[type]) {
|
|
166
|
-
this.messageHandlers[type](socket, data);
|
|
167
|
-
} else {
|
|
168
|
-
console.warn(`Unhandled message type from ${ip}: ${type}`);
|
|
169
|
-
socket.close();
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Handles socket disconnection.
|
|
175
|
-
* @param {WebSocket} socket - The WebSocket connection instance.
|
|
176
|
-
* @param {string} ip - The client's IP address.
|
|
177
|
-
*/
|
|
178
|
-
handleClose(socket, ip) {
|
|
179
|
-
console.log(`Client disconnected: ${ip}`);
|
|
180
|
-
this.clients.delete(ip);
|
|
181
|
-
if (this.connectionCloseCallback) this.connectionCloseCallback(socket);
|
|
36
|
+
this.server = server;
|
|
37
|
+
if (!options.routes?.length) options.routes = [ DefaultRoute ];
|
|
38
|
+
this.routes = options.routes.map((route) => new route(server));
|
|
39
|
+
this.server.on('upgrade', this.handleUpgrade.bind(this));
|
|
182
40
|
}
|
|
183
41
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
* @param {string} ip - The client's IP address.
|
|
189
|
-
*/
|
|
190
|
-
handleError(socket, error, ip) {
|
|
191
|
-
console.error(`Socket error from ${ip}:`, error);
|
|
42
|
+
handleUpgrade(req, sock, head) {
|
|
43
|
+
const route = this.routes.find(route => route.path == req.url);
|
|
44
|
+
if (!route) sock.destroy();
|
|
45
|
+
else route.server.handleUpgrade(req, sock, head, (s, r) => route.server.emit('connection', s, r));
|
|
192
46
|
}
|
|
193
47
|
|
|
194
48
|
/**
|
|
195
|
-
*
|
|
196
|
-
* @param {
|
|
49
|
+
*
|
|
50
|
+
* @param {new () => import('./SocketRoute')} route
|
|
197
51
|
*/
|
|
198
|
-
|
|
199
|
-
|
|
52
|
+
addRoute(route) {
|
|
53
|
+
this.routes.push(new route(this.server));
|
|
200
54
|
}
|
|
201
55
|
}
|
|
202
56
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const { BaseHandler } = require("./BaseHandler");
|
|
2
|
+
const { sendJson } = require("./util");
|
|
3
|
+
|
|
4
|
+
class DefaultHandler extends BaseHandler {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("DefaultHandler");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
onMessage(socket, message) {
|
|
10
|
+
socket.send(sendJson(`I got your message of ${JSON.stringify(message)}`));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = DefaultHandler;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const SocketRoute = require("./SocketRoute");
|
|
2
|
+
const DefaultHandler = require('./DefaultHandler');
|
|
3
|
+
|
|
4
|
+
class DefaultRoute extends SocketRoute {
|
|
5
|
+
constructor(server) {
|
|
6
|
+
super({
|
|
7
|
+
server,
|
|
8
|
+
path: "/",
|
|
9
|
+
handlers: [DefaultHandler]
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = DefaultRoute;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const { WebSocketServer } = require("ws");
|
|
2
|
+
const { sendJson, broadcast } = require("./util");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Represents a WebSocket route configuration.
|
|
6
|
+
* This class is used to define a specific WebSocket endpoint (`path`) and its associated handlers.
|
|
7
|
+
*/
|
|
8
|
+
class SocketRoute {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new instance of `SocketRoute`.
|
|
11
|
+
* @param {Object} options - Configuration options for the WebSocket route.
|
|
12
|
+
* @param {string} options.path - The path of the WebSocket route (e.g., `/chat`, `/lobby`).
|
|
13
|
+
* @param {import('./BaseHandler').BaseHandler[]} options.handlers - An array of handler instances that manage connections and messages for this route.
|
|
14
|
+
*/
|
|
15
|
+
constructor({path, handlers } = {}) {
|
|
16
|
+
if (!path) {
|
|
17
|
+
throw new Error('A `path` must be specified for the SocketRoute.');
|
|
18
|
+
}
|
|
19
|
+
if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
|
|
20
|
+
throw new Error('At least one handler must be specified for the SocketRoute.');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The path of the WebSocket route.
|
|
24
|
+
* This determines the endpoint that clients must connect to (e.g., `ws://localhost:3000/chat`).
|
|
25
|
+
* @type {string}
|
|
26
|
+
*/
|
|
27
|
+
this.path = path;
|
|
28
|
+
/**
|
|
29
|
+
* The array of handler instances associated with this route.
|
|
30
|
+
* Each handler is responsible for managing WebSocket connections and message handling logic.
|
|
31
|
+
* @type {import('./BaseHandler').BaseHandler[]}
|
|
32
|
+
*/
|
|
33
|
+
this.handlers = handlers.map(HandlerClass => new HandlerClass());
|
|
34
|
+
this.clients = new Map();
|
|
35
|
+
this.server = new WebSocketServer({ noServer: true, path });
|
|
36
|
+
this.server.on('connection', this.handleConnection.bind(this));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Adds a new handler to the WebSocket server.
|
|
40
|
+
* @param {new () => BaseHandler} HandlerClass - The handler class to add.
|
|
41
|
+
*/
|
|
42
|
+
addHandler(HandlerClass) {
|
|
43
|
+
const newHandler = new HandlerClass();
|
|
44
|
+
if (this.handlers.find(handler => handler.name === newHandler.name)) {
|
|
45
|
+
console.warn(`Handler with name '${newHandler.name}' already exists.`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.handlers.push(newHandler);
|
|
49
|
+
console.log(`Handler '${newHandler.name}' added successfully.`);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Handles a new WebSocket connection.
|
|
53
|
+
* @param {WebSocket} socket - The WebSocket connection instance.
|
|
54
|
+
* @param {import('http').IncomingMessage} req - The HTTP request object associated with the connection.
|
|
55
|
+
*/
|
|
56
|
+
handleConnection(socket, req) {
|
|
57
|
+
const ip = req.socket.remoteAddress;
|
|
58
|
+
console.log(`New client connected: ${ip}`);
|
|
59
|
+
if (this.clients.get(ip) !== undefined) {
|
|
60
|
+
const oldClient = this.clients.get(ip);
|
|
61
|
+
console.warn(`Client ${ip} already connected, disconnecting existing connection.`);
|
|
62
|
+
oldClient.send(
|
|
63
|
+
JSON.stringify({ msg: 'You are being disconnected because a new client is connected with your IP address.' })
|
|
64
|
+
);
|
|
65
|
+
oldClient.close();
|
|
66
|
+
}
|
|
67
|
+
this.clients.set(ip, socket);
|
|
68
|
+
socket.isAssigned = false; // Tracks whether the socket has been assigned a handler.
|
|
69
|
+
socket.sendJson = (data) => sendJson(socket, data);
|
|
70
|
+
socket.broadcast = (data) => broadcast([...this.clients.values()], data);
|
|
71
|
+
|
|
72
|
+
this.connectionOpenCallback(socket);
|
|
73
|
+
socket.on('message', (message) => this.handleMessage(socket, JSON.parse(message)));
|
|
74
|
+
socket.on('close', this.handleClose.bind(this));
|
|
75
|
+
socket.on('error', this.handleError.bind(this));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
connectionOpenCallback(socket) {
|
|
79
|
+
console.log(`Opening new connection: ${socket.remoteAddress}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
handleMessage(sock, data) {
|
|
83
|
+
const handler = this.handlers.find((handler) => handler.name == data.type);
|
|
84
|
+
if (!handler) {
|
|
85
|
+
sendJson(sock, {error: `No such handler ${data.type}`});
|
|
86
|
+
sock.close();
|
|
87
|
+
} else {
|
|
88
|
+
handler.handleMessage(sock, data);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Handles socket disconnection.
|
|
94
|
+
* @param {WebSocket} socket - The WebSocket connection instance.
|
|
95
|
+
* @param {string} ip - The client's IP address.
|
|
96
|
+
*/
|
|
97
|
+
handleClose(socket, ip) {
|
|
98
|
+
console.log(`Client disconnected: ${ip}`);
|
|
99
|
+
this.clients.delete(ip);
|
|
100
|
+
if (this.connectionCloseCallback) this.connectionCloseCallback(socket);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Handles socket errors.
|
|
105
|
+
* @param {WebSocket} socket - The WebSocket connection instance.
|
|
106
|
+
* @param {Error} error - The error object.
|
|
107
|
+
* @param {string} ip - The client's IP address.
|
|
108
|
+
*/
|
|
109
|
+
handleError(socket, error, ip) {
|
|
110
|
+
console.error(`Socket error from ${ip}:`, error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = SocketRoute;
|
package/src/ws/index.js
CHANGED