@utsp/network-server 0.1.1
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 +36 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +571 -0
- package/dist/index.mjs +1 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @utsp/network-server
|
|
2
|
+
|
|
3
|
+
> ⚠️ **PROTOTYPE - NOT READY FOR PRODUCTION**
|
|
4
|
+
>
|
|
5
|
+
> This package is currently in early development and should **NOT** be used in production.
|
|
6
|
+
> The API is unstable and subject to breaking changes without notice.
|
|
7
|
+
|
|
8
|
+
Server-side network communication layer for UTSP (Universal Text Stream Protocol).
|
|
9
|
+
|
|
10
|
+
[](https://opensource.org/licenses/MIT)
|
|
11
|
+
|
|
12
|
+
## ⚠️ Development Status
|
|
13
|
+
|
|
14
|
+
**This is a prototype package under active development.**
|
|
15
|
+
|
|
16
|
+
- ❌ No stable API
|
|
17
|
+
- ❌ No documentation available yet
|
|
18
|
+
- ❌ Breaking changes expected
|
|
19
|
+
- ❌ Not recommended for production use
|
|
20
|
+
|
|
21
|
+
**Please check back later for updates or watch the repository for release announcements.**
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @utsp/network-server
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Repository
|
|
30
|
+
|
|
31
|
+
- [GitHub](https://github.com/thp-software/utsp)
|
|
32
|
+
- [Issues](https://github.com/thp-software/utsp/issues)
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
MIT © 2025 Thomas Piquet
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var o=Object.defineProperty;var u=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var f=Object.prototype.hasOwnProperty;var S=(r,e,t)=>e in r?o(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var h=(r,e)=>o(r,"name",{value:e,configurable:!0});var C=(r,e)=>{for(var t in e)o(r,t,{get:e[t],enumerable:!0})},y=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of m(e))!f.call(r,i)&&i!==t&&o(r,i,{get:()=>e[i],enumerable:!(n=u(e,i))||n.enumerable});return r};var H=r=>y(o({},"__esModule",{value:!0}),r);var s=(r,e,t)=>(S(r,typeof e!="symbol"?e+"":e,t),t);var $={};C($,{SocketIOServer:()=>a});module.exports=H($);var d=require("socket.io"),g=require("http");var c=class c{constructor(e){s(this,"io",null);s(this,"httpServer",null);s(this,"options");s(this,"running",!1);s(this,"clients",new Map);s(this,"clientData",new Map);s(this,"connectionHandlers",[]);s(this,"disconnectionHandlers",[]);s(this,"eventHandlers",new Map);s(this,"stats",{totalConnections:0,startTime:0});if(!Number.isInteger(e.port)||e.port<0||e.port>65535)throw new Error(`SocketIOServer: Invalid port ${e.port} - must be an integer between 0 and 65535`);if(e.maxConnections!==void 0&&(!Number.isInteger(e.maxConnections)||e.maxConnections<=0))throw new Error(`SocketIOServer: Invalid maxConnections ${e.maxConnections} - must be a positive integer`);if(e.pingInterval!==void 0&&(!Number.isFinite(e.pingInterval)||e.pingInterval<=0))throw new Error(`SocketIOServer: Invalid pingInterval ${e.pingInterval} - must be a positive number`);if(e.pingTimeout!==void 0&&(!Number.isFinite(e.pingTimeout)||e.pingTimeout<=0))throw new Error(`SocketIOServer: Invalid pingTimeout ${e.pingTimeout} - must be a positive number`);this.options={port:e.port,host:e.host??"0.0.0.0",cors:e.cors??{origin:"*"},maxConnections:e.maxConnections??1e3,pingInterval:e.pingInterval??25e3,pingTimeout:e.pingTimeout??5e3,debug:e.debug??!1},this.log("Server initialized",{port:this.options.port,host:this.options.host,maxConnections:this.options.maxConnections})}isRunning(){return this.running}async start(){if(this.running){this.log("Server already running");return}return this.log(`Starting server on ${this.options.host}:${this.options.port}...`),new Promise((e,t)=>{try{this.httpServer=(0,g.createServer)(),this.io=new d.Server(this.httpServer,{cors:this.options.cors,pingInterval:this.options.pingInterval,pingTimeout:this.options.pingTimeout,maxHttpBufferSize:1e6}),this.io.on("connection",n=>{this.handleConnection(n)}),this.httpServer.listen(this.options.port,this.options.host,()=>{this.running=!0,this.stats.startTime=Date.now(),this.log(`Server started on ${this.options.host}:${this.options.port}`),e()}),this.httpServer.on("error",n=>{this.log(`Server error: ${n.message}`),t(n)})}catch(n){t(n)}})}async stop(){if(this.running)return this.log("Stopping server..."),new Promise(e=>{this.clients.forEach(t=>{t.disconnect(!0)}),this.clients.clear(),this.clientData.clear(),this.io&&(this.io.close(()=>{this.log("Socket.IO server closed")}),this.io=null),this.httpServer?(this.httpServer.close(()=>{this.log("HTTP server closed"),this.running=!1,e()}),this.httpServer=null):(this.running=!1,e())})}getClients(){return Array.from(this.clients.keys())}getClientInfo(e){let t=this.clients.get(e);return t?{id:e,connectedAt:t.connectedAt||Date.now(),address:t.handshake.address,data:this.clientData.get(e)||{}}:null}sendToClient(e,t,n){let i=this.clients.get(e);if(!i){this.log(`Cannot send to client ${e}: not found`);return}i.emit(t,n)}sendToClientVolatile(e,t,n){let i=this.clients.get(e);if(!i){this.log(`Cannot send volatile to client ${e}: not found`);return}i.compress(!1).emit(t,n)}broadcast(e,t){this.io&&(this.io.emit(e,t),this.log(`Broadcast '${e}' to all clients`))}broadcastVolatile(e,t){this.io&&(this.io.volatile.emit(e,t),this.log(`Broadcast volatile '${e}' to all clients`))}broadcastExcept(e,t,n){let i=this.clients.get(e);i&&(i.broadcast.emit(t,n),this.log(`Broadcast '${t}' except ${e}`))}sendToRoom(e,t,n){this.io&&(this.io.to(e).emit(t,n),this.log(`Sent '${t}' to room '${e}'`))}joinRoom(e,t){let n=this.clients.get(e);n&&(n.join(t),this.log(`Client ${e} joined room '${t}'`))}leaveRoom(e,t){let n=this.clients.get(e);n&&(n.leave(t),this.log(`Client ${e} left room '${t}'`))}getRoomClients(e){if(!this.io)return[];let t=this.io.sockets.adapter.rooms.get(e);return t?Array.from(t):[]}disconnectClient(e,t="Server disconnect"){let n=this.clients.get(e);n&&(n.disconnect(!0),this.log(`Disconnected client ${e}: ${t}`))}on(e,t){let n=this.eventHandlers.get(e)||[];n.push(t),this.eventHandlers.set(e,n),this.log(`Registered handler for '${e}'`)}onConnect(e){this.connectionHandlers.push(e),this.log("Registered connection handler")}onDisconnect(e){this.disconnectionHandlers.push(e),this.log("Registered disconnection handler")}off(e,t){let n=this.eventHandlers.get(e);if(!n)return;let i=n.indexOf(t);i!==-1&&(n.splice(i,1),this.log(`Removed handler for '${e}'`))}setClientData(e,t,n){let i=this.clientData.get(e)||{};i[t]=n,this.clientData.set(e,i)}getClientData(e,t){let n=this.clientData.get(e);return n?n[t]:void 0}getStats(){return{connectedClients:this.clients.size,totalConnections:this.stats.totalConnections,uptime:Date.now()-this.stats.startTime}}async destroy(){await this.stop(),this.connectionHandlers=[],this.disconnectionHandlers=[],this.eventHandlers.clear(),this.log("Server destroyed")}handleConnection(e){let t=e.id;if(this.log(`Client connected: ${t}`),this.clients.size>=this.options.maxConnections){this.log(`Max connections reached, rejecting ${t}`),e.emit("error",{code:"MAX_CONNECTIONS",message:"Server is full"}),e.disconnect(!0);return}this.clients.set(t,e),this.clientData.set(t,{}),e.connectedAt=Date.now(),this.stats.totalConnections++,this.setupClientHandlers(e),this.connectionHandlers.forEach(n=>{try{n(t)}catch(i){this.log(`Error in connection handler: ${i}`)}}),e.on("ping",()=>{e.emit("pong")})}setupClientHandlers(e){let t=e.id;e.on("disconnect",n=>{this.log(`Client disconnected: ${t} (${n})`),this.clients.delete(t),this.clientData.delete(t),this.disconnectionHandlers.forEach(i=>{try{i(t,n)}catch(l){this.log(`Error in disconnection handler: ${l}`)}})}),this.eventHandlers.forEach((n,i)=>{e.on(i,l=>{n.forEach(v=>{try{v(t,l)}catch(p){this.log(`Error in event handler for '${i}': ${p}`)}})})})}log(e,t){this.options.debug&&(t!==void 0?console.warn(`[SocketIOServer] ${e}`,t):console.warn(`[SocketIOServer] ${e}`))}};h(c,"SocketIOServer");var a=c;0&&(module.exports={SocketIOServer});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import { INetworkServer, NetworkServerOptions, ClientInfo, ServerEventHandler, ConnectionHandler, DisconnectionHandler } from '@utsp/types';
|
|
2
|
+
export { AnyNetworkMessage, ChatMessage, ClientInfo, ConnectionHandler, DisconnectionHandler, ErrorMessage, INetworkServer, InputMessage, JoinMessage, JoinResponseMessage, LeaveMessage, LoadMessage, MessageType, NetworkMessage, NetworkServerOptions, PingMessage, PongMessage, ServerEventHandler, UpdateMessage } from '@utsp/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Socket.IO Server Implementation
|
|
6
|
+
*
|
|
7
|
+
* Production-ready Socket.IO server for UTSP real-time networking.
|
|
8
|
+
* Implements the INetworkServer interface with room management, client tracking,
|
|
9
|
+
* and comprehensive event handling. Node.js only.
|
|
10
|
+
*
|
|
11
|
+
* @example Basic Server
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { SocketIOServer } from '@utsp/network-server';
|
|
14
|
+
*
|
|
15
|
+
* const server = new SocketIOServer({
|
|
16
|
+
* port: 3000,
|
|
17
|
+
* host: '0.0.0.0',
|
|
18
|
+
* debug: false
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* await server.start();
|
|
22
|
+
* console.log('Server running on port 3000');
|
|
23
|
+
*
|
|
24
|
+
* server.onConnect((clientId) => {
|
|
25
|
+
* console.log('Client connected:', clientId);
|
|
26
|
+
* server.sendToClient(clientId, 'welcome', { message: 'Hello!' });
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* server.on('input', (clientId, data) => {
|
|
30
|
+
* console.log('Input from', clientId, data);
|
|
31
|
+
* server.broadcast('update', data);
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @example With Rooms
|
|
36
|
+
* ```typescript
|
|
37
|
+
* server.onConnect((clientId) => {
|
|
38
|
+
* server.joinRoom(clientId, 'lobby');
|
|
39
|
+
* server.sendToRoom('lobby', 'playerJoined', { clientId });
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* server.on('joinGame', (clientId, { gameId }) => {
|
|
43
|
+
* server.leaveRoom(clientId, 'lobby');
|
|
44
|
+
* server.joinRoom(clientId, `game-${gameId}`);
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @example Client Data Storage
|
|
49
|
+
* ```typescript
|
|
50
|
+
* server.onConnect((clientId) => {
|
|
51
|
+
* server.setClientData(clientId, 'username', 'Player1');
|
|
52
|
+
* server.setClientData(clientId, 'score', 0);
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* server.on('updateScore', (clientId, { points }) => {
|
|
56
|
+
* const currentScore = server.getClientData(clientId, 'score') || 0;
|
|
57
|
+
* server.setClientData(clientId, 'score', currentScore + points);
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Socket.IO server implementation for UTSP network communication
|
|
64
|
+
*
|
|
65
|
+
* Features:
|
|
66
|
+
* - Connection management with max connections limit
|
|
67
|
+
* - Room-based broadcasting
|
|
68
|
+
* - Client data storage per connection
|
|
69
|
+
* - Volatile messaging for high-frequency updates
|
|
70
|
+
* - Comprehensive event handling system
|
|
71
|
+
* - Connection/disconnection lifecycle hooks
|
|
72
|
+
* - Server statistics tracking
|
|
73
|
+
* - CORS configuration
|
|
74
|
+
*
|
|
75
|
+
* @implements {INetworkServer}
|
|
76
|
+
*/
|
|
77
|
+
declare class SocketIOServer implements INetworkServer {
|
|
78
|
+
private io;
|
|
79
|
+
private httpServer;
|
|
80
|
+
private options;
|
|
81
|
+
private running;
|
|
82
|
+
private clients;
|
|
83
|
+
private clientData;
|
|
84
|
+
private connectionHandlers;
|
|
85
|
+
private disconnectionHandlers;
|
|
86
|
+
private eventHandlers;
|
|
87
|
+
private stats;
|
|
88
|
+
/**
|
|
89
|
+
* Create a new Socket.IO server instance
|
|
90
|
+
*
|
|
91
|
+
* @param options - Server configuration options
|
|
92
|
+
* @throws {Error} If port is invalid or options are malformed
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* const server = new SocketIOServer({
|
|
97
|
+
* port: 3000,
|
|
98
|
+
* host: '0.0.0.0',
|
|
99
|
+
* cors: { origin: '*' },
|
|
100
|
+
* maxConnections: 1000,
|
|
101
|
+
* pingInterval: 25000,
|
|
102
|
+
* pingTimeout: 5000,
|
|
103
|
+
* debug: true
|
|
104
|
+
* });
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
constructor(options: NetworkServerOptions);
|
|
108
|
+
/**
|
|
109
|
+
* Check if server is currently running
|
|
110
|
+
*
|
|
111
|
+
* @returns true if server is running and accepting connections
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* if (server.isRunning()) {
|
|
116
|
+
* console.log('Server is active');
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
isRunning(): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Start the server and begin accepting connections
|
|
123
|
+
*
|
|
124
|
+
* Creates an HTTP server, initializes Socket.IO, and starts listening
|
|
125
|
+
* on the configured port and host.
|
|
126
|
+
*
|
|
127
|
+
* @returns Promise resolving when server is ready to accept connections
|
|
128
|
+
* @throws {Error} If server fails to start (port in use, permission denied, etc.)
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* try {
|
|
133
|
+
* await server.start();
|
|
134
|
+
* console.log('Server started successfully');
|
|
135
|
+
* } catch (error) {
|
|
136
|
+
* console.error('Failed to start server:', error);
|
|
137
|
+
* }
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
start(): Promise<void>;
|
|
141
|
+
/**
|
|
142
|
+
* Stop the server and disconnect all clients
|
|
143
|
+
*
|
|
144
|
+
* Gracefully shuts down the server by disconnecting all clients,
|
|
145
|
+
* closing Socket.IO, and stopping the HTTP server.
|
|
146
|
+
*
|
|
147
|
+
* @returns Promise resolving when server is fully stopped
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* await server.stop();
|
|
152
|
+
* console.log('Server stopped');
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
stop(): Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Get list of all connected client IDs
|
|
158
|
+
*
|
|
159
|
+
* @returns Array of client socket IDs
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const clients = server.getClients();
|
|
164
|
+
* console.log(`${clients.length} clients connected`);
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
getClients(): string[];
|
|
168
|
+
/**
|
|
169
|
+
* Get information about a specific client
|
|
170
|
+
*
|
|
171
|
+
* @param clientId - Client socket ID
|
|
172
|
+
* @returns Client information object, or null if client not found
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```typescript
|
|
176
|
+
* const info = server.getClientInfo(clientId);
|
|
177
|
+
* if (info) {
|
|
178
|
+
* console.log('Client:', info.id);
|
|
179
|
+
* console.log('Connected at:', new Date(info.connectedAt));
|
|
180
|
+
* console.log('Address:', info.address);
|
|
181
|
+
* console.log('Data:', info.data);
|
|
182
|
+
* }
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
getClientInfo(clientId: string): ClientInfo | null;
|
|
186
|
+
/**
|
|
187
|
+
* Send an event to a specific client
|
|
188
|
+
*
|
|
189
|
+
* Sends a message to a single client identified by their socket ID.
|
|
190
|
+
* Message is silently dropped if client is not connected.
|
|
191
|
+
*
|
|
192
|
+
* @param clientId - Client socket ID
|
|
193
|
+
* @param event - Event name
|
|
194
|
+
* @param data - Event payload (any serializable data)
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```typescript
|
|
198
|
+
* server.sendToClient(clientId, 'notification', {
|
|
199
|
+
* type: 'info',
|
|
200
|
+
* message: 'You won!'
|
|
201
|
+
* });
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
sendToClient(clientId: string, event: string, data: any): void;
|
|
205
|
+
/**
|
|
206
|
+
* Send volatile message to a specific client
|
|
207
|
+
*
|
|
208
|
+
* Sends a "volatile" message that can be dropped if the network is congested.
|
|
209
|
+
* Perfect for high-frequency updates (game state, animations, dynamic layers)
|
|
210
|
+
* where missing a frame is acceptable.
|
|
211
|
+
*
|
|
212
|
+
* **Note:** Uses `.compress(false)` instead of `.volatile` due to Socket.IO limitations.
|
|
213
|
+
* This disables compression for better performance with frequent updates.
|
|
214
|
+
*
|
|
215
|
+
* @param clientId - Client socket ID
|
|
216
|
+
* @param event - Event name
|
|
217
|
+
* @param data - Event payload
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```typescript
|
|
221
|
+
* // Send 60 updates per second - some can be dropped
|
|
222
|
+
* setInterval(() => {
|
|
223
|
+
* server.sendToClientVolatile(clientId, 'gameState', {
|
|
224
|
+
* position: player.position,
|
|
225
|
+
* velocity: player.velocity
|
|
226
|
+
* });
|
|
227
|
+
* }, 16); // ~60 FPS
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
sendToClientVolatile(clientId: string, event: string, data: any): void;
|
|
231
|
+
/**
|
|
232
|
+
* Broadcast an event to all connected clients
|
|
233
|
+
*
|
|
234
|
+
* Sends a message to every connected client simultaneously.
|
|
235
|
+
*
|
|
236
|
+
* @param event - Event name
|
|
237
|
+
* @param data - Event payload
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```typescript
|
|
241
|
+
* server.broadcast('serverMessage', {
|
|
242
|
+
* type: 'announcement',
|
|
243
|
+
* message: 'Server restarting in 5 minutes'
|
|
244
|
+
* });
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
broadcast(event: string, data: any): void;
|
|
248
|
+
/**
|
|
249
|
+
* Broadcast volatile message to all clients
|
|
250
|
+
*
|
|
251
|
+
* Sends a volatile message to all clients. Messages can be dropped
|
|
252
|
+
* if the network is congested. Useful for high-frequency broadcasts.
|
|
253
|
+
*
|
|
254
|
+
* @param event - Event name
|
|
255
|
+
* @param data - Event payload
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```typescript
|
|
259
|
+
* // Broadcast game tick (60 times per second)
|
|
260
|
+
* setInterval(() => {
|
|
261
|
+
* server.broadcastVolatile('tick', {
|
|
262
|
+
* timestamp: Date.now(),
|
|
263
|
+
* frame: frameCount++
|
|
264
|
+
* });
|
|
265
|
+
* }, 16);
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
broadcastVolatile(event: string, data: any): void;
|
|
269
|
+
/**
|
|
270
|
+
* Broadcast to all clients except one
|
|
271
|
+
*
|
|
272
|
+
* Sends a message to all connected clients except the specified one.
|
|
273
|
+
* Useful for broadcasting player actions to other players.
|
|
274
|
+
*
|
|
275
|
+
* @param excludeClientId - Client ID to exclude from broadcast
|
|
276
|
+
* @param event - Event name
|
|
277
|
+
* @param data - Event payload
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* ```typescript
|
|
281
|
+
* // When a player moves, tell everyone else
|
|
282
|
+
* server.on('playerMove', (clientId, position) => {
|
|
283
|
+
* server.broadcastExcept(clientId, 'playerMoved', {
|
|
284
|
+
* playerId: clientId,
|
|
285
|
+
* position
|
|
286
|
+
* });
|
|
287
|
+
* });
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
broadcastExcept(excludeClientId: string, event: string, data: any): void;
|
|
291
|
+
/**
|
|
292
|
+
* Send event to all clients in a room
|
|
293
|
+
*
|
|
294
|
+
* Broadcasts a message to all clients that have joined the specified room.
|
|
295
|
+
*
|
|
296
|
+
* @param room - Room name
|
|
297
|
+
* @param event - Event name
|
|
298
|
+
* @param data - Event payload
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* // Send message to all players in a game
|
|
303
|
+
* server.sendToRoom('game-123', 'gameUpdate', {
|
|
304
|
+
* score: { team1: 10, team2: 8 }
|
|
305
|
+
* });
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
sendToRoom(room: string, event: string, data: any): void;
|
|
309
|
+
/**
|
|
310
|
+
* Add a client to a room
|
|
311
|
+
*
|
|
312
|
+
* Rooms allow grouping clients for targeted broadcasts. A client
|
|
313
|
+
* can be in multiple rooms simultaneously.
|
|
314
|
+
*
|
|
315
|
+
* @param clientId - Client socket ID
|
|
316
|
+
* @param room - Room name
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* ```typescript
|
|
320
|
+
* // When player joins a game
|
|
321
|
+
* server.on('joinGame', (clientId, { gameId }) => {
|
|
322
|
+
* server.joinRoom(clientId, `game-${gameId}`);
|
|
323
|
+
* server.sendToRoom(`game-${gameId}`, 'playerJoined', {
|
|
324
|
+
* clientId,
|
|
325
|
+
* playerCount: server.getRoomClients(`game-${gameId}`).length
|
|
326
|
+
* });
|
|
327
|
+
* });
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
joinRoom(clientId: string, room: string): void;
|
|
331
|
+
/**
|
|
332
|
+
* Remove a client from a room
|
|
333
|
+
*
|
|
334
|
+
* @param clientId - Client socket ID
|
|
335
|
+
* @param room - Room name
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```typescript
|
|
339
|
+
* server.on('leaveGame', (clientId) => {
|
|
340
|
+
* server.leaveRoom(clientId, 'game-123');
|
|
341
|
+
* server.joinRoom(clientId, 'lobby');
|
|
342
|
+
* });
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
leaveRoom(clientId: string, room: string): void;
|
|
346
|
+
/**
|
|
347
|
+
* Get list of all client IDs in a room
|
|
348
|
+
*
|
|
349
|
+
* @param room - Room name
|
|
350
|
+
* @returns Array of client socket IDs in the room
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```typescript
|
|
354
|
+
* const players = server.getRoomClients('game-123');
|
|
355
|
+
* console.log(`${players.length} players in game`);
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
getRoomClients(room: string): string[];
|
|
359
|
+
/**
|
|
360
|
+
* Forcefully disconnect a client
|
|
361
|
+
*
|
|
362
|
+
* Immediately closes the client's connection. Use sparingly as it
|
|
363
|
+
* does not allow graceful cleanup on the client side.
|
|
364
|
+
*
|
|
365
|
+
* @param clientId - Client socket ID
|
|
366
|
+
* @param reason - Reason for disconnection (default: 'Server disconnect')
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```typescript
|
|
370
|
+
* // Kick idle players
|
|
371
|
+
* if (isIdle(clientId)) {
|
|
372
|
+
* server.disconnectClient(clientId, 'Idle timeout');
|
|
373
|
+
* }
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
376
|
+
disconnectClient(clientId: string, reason?: string): void;
|
|
377
|
+
/**
|
|
378
|
+
* Register an event listener for client messages
|
|
379
|
+
*
|
|
380
|
+
* Handles events sent from clients. Multiple handlers can be registered
|
|
381
|
+
* for the same event. Handlers receive the client ID and event data.
|
|
382
|
+
*
|
|
383
|
+
* @param event - Event name to listen for
|
|
384
|
+
* @param handler - Callback function (clientId, data) => void
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```typescript
|
|
388
|
+
* server.on<InputMessage>('input', (clientId, data) => {
|
|
389
|
+
* console.log('Input from', clientId, data);
|
|
390
|
+
* // Process input and broadcast to others
|
|
391
|
+
* server.broadcastExcept(clientId, 'playerAction', data);
|
|
392
|
+
* });
|
|
393
|
+
*
|
|
394
|
+
* server.on('chat', (clientId, message) => {
|
|
395
|
+
* const username = server.getClientData(clientId, 'username');
|
|
396
|
+
* server.broadcast('chatMessage', { username, message });
|
|
397
|
+
* });
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
on<T = any>(event: string, handler: ServerEventHandler<T>): void;
|
|
401
|
+
/**
|
|
402
|
+
* Register a connection handler
|
|
403
|
+
*
|
|
404
|
+
* Called whenever a new client successfully connects. Use this to
|
|
405
|
+
* initialize client state, send welcome messages, etc.
|
|
406
|
+
*
|
|
407
|
+
* @param handler - Callback function receiving client ID
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* ```typescript
|
|
411
|
+
* server.onConnect((clientId) => {
|
|
412
|
+
* console.log('Client connected:', clientId);
|
|
413
|
+
* server.setClientData(clientId, 'joinedAt', Date.now());
|
|
414
|
+
* server.joinRoom(clientId, 'lobby');
|
|
415
|
+
* server.sendToClient(clientId, 'welcome', {
|
|
416
|
+
* message: 'Welcome to the server!',
|
|
417
|
+
* serverId: 'game-server-1'
|
|
418
|
+
* });
|
|
419
|
+
* });
|
|
420
|
+
* ```
|
|
421
|
+
*/
|
|
422
|
+
onConnect(handler: ConnectionHandler): void;
|
|
423
|
+
/**
|
|
424
|
+
* Register a disconnection handler
|
|
425
|
+
*
|
|
426
|
+
* Called whenever a client disconnects (gracefully or due to error).
|
|
427
|
+
* Use this for cleanup and notifying other clients.
|
|
428
|
+
*
|
|
429
|
+
* @param handler - Callback function receiving client ID and reason
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* ```typescript
|
|
433
|
+
* server.onDisconnect((clientId, reason) => {
|
|
434
|
+
* console.log('Client disconnected:', clientId, reason);
|
|
435
|
+
* const username = server.getClientData(clientId, 'username');
|
|
436
|
+
* server.broadcast('playerLeft', { username, reason });
|
|
437
|
+
* });
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
onDisconnect(handler: DisconnectionHandler): void;
|
|
441
|
+
/**
|
|
442
|
+
* Unregister a specific event handler
|
|
443
|
+
*
|
|
444
|
+
* Removes a previously registered handler for an event. The handler
|
|
445
|
+
* reference must be the exact same function object that was registered.
|
|
446
|
+
*
|
|
447
|
+
* @param event - Event name
|
|
448
|
+
* @param handler - Handler function to remove (must be same reference)
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* ```typescript
|
|
452
|
+
* const handler = (clientId, data) => { ... };
|
|
453
|
+
* server.on('input', handler);
|
|
454
|
+
* // Later...
|
|
455
|
+
* server.off('input', handler);
|
|
456
|
+
* ```
|
|
457
|
+
*/
|
|
458
|
+
off<T = any>(event: string, handler: ServerEventHandler<T>): void;
|
|
459
|
+
/**
|
|
460
|
+
* Store arbitrary data for a client
|
|
461
|
+
*
|
|
462
|
+
* Associates key-value data with a client connection. Data is
|
|
463
|
+
* automatically cleared when the client disconnects.
|
|
464
|
+
*
|
|
465
|
+
* @param clientId - Client socket ID
|
|
466
|
+
* @param key - Data key
|
|
467
|
+
* @param value - Data value (any serializable type)
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* ```typescript
|
|
471
|
+
* server.onConnect((clientId) => {
|
|
472
|
+
* server.setClientData(clientId, 'username', 'Player1');
|
|
473
|
+
* server.setClientData(clientId, 'score', 0);
|
|
474
|
+
* server.setClientData(clientId, 'team', 'red');
|
|
475
|
+
* });
|
|
476
|
+
*
|
|
477
|
+
* server.on('scorePoint', (clientId) => {
|
|
478
|
+
* const score = server.getClientData(clientId, 'score') || 0;
|
|
479
|
+
* server.setClientData(clientId, 'score', score + 1);
|
|
480
|
+
* });
|
|
481
|
+
* ```
|
|
482
|
+
*/
|
|
483
|
+
setClientData(clientId: string, key: string, value: any): void;
|
|
484
|
+
/**
|
|
485
|
+
* Retrieve stored data for a client
|
|
486
|
+
*
|
|
487
|
+
* @param clientId - Client socket ID
|
|
488
|
+
* @param key - Data key
|
|
489
|
+
* @returns Stored value, or undefined if not found
|
|
490
|
+
*
|
|
491
|
+
* @example
|
|
492
|
+
* ```typescript
|
|
493
|
+
* const username = server.getClientData(clientId, 'username');
|
|
494
|
+
* const score = server.getClientData(clientId, 'score') || 0;
|
|
495
|
+
* ```
|
|
496
|
+
*/
|
|
497
|
+
getClientData(clientId: string, key: string): any;
|
|
498
|
+
/**
|
|
499
|
+
* Get server statistics
|
|
500
|
+
*
|
|
501
|
+
* Returns current server metrics including connected clients,
|
|
502
|
+
* total connections since start, and uptime.
|
|
503
|
+
*
|
|
504
|
+
* @returns Object containing server stats
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* ```typescript
|
|
508
|
+
* const stats = server.getStats();
|
|
509
|
+
* console.log('Connected:', stats.connectedClients);
|
|
510
|
+
* console.log('Total connections:', stats.totalConnections);
|
|
511
|
+
* console.log('Uptime:', Math.floor(stats.uptime / 1000), 'seconds');
|
|
512
|
+
* ```
|
|
513
|
+
*/
|
|
514
|
+
getStats(): {
|
|
515
|
+
connectedClients: number;
|
|
516
|
+
totalConnections: number;
|
|
517
|
+
uptime: number;
|
|
518
|
+
};
|
|
519
|
+
/**
|
|
520
|
+
* Destroy the server and clean up all resources
|
|
521
|
+
*
|
|
522
|
+
* Stops the server, disconnects all clients, and clears all handlers.
|
|
523
|
+
* Server instance cannot be reused after calling destroy().
|
|
524
|
+
*
|
|
525
|
+
* @returns Promise resolving when cleanup is complete
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```typescript
|
|
529
|
+
* // Graceful shutdown
|
|
530
|
+
* process.on('SIGTERM', async () => {
|
|
531
|
+
* console.log('Shutting down...');
|
|
532
|
+
* await server.destroy();
|
|
533
|
+
* process.exit(0);
|
|
534
|
+
* });
|
|
535
|
+
* ```
|
|
536
|
+
*/
|
|
537
|
+
destroy(): Promise<void>;
|
|
538
|
+
/**
|
|
539
|
+
* Handle new client connection
|
|
540
|
+
*
|
|
541
|
+
* Called when a client successfully connects. Checks max connections limit,
|
|
542
|
+
* stores client data, sets up event handlers, and calls connection handlers.
|
|
543
|
+
*
|
|
544
|
+
* @param socket - Socket.IO socket instance
|
|
545
|
+
* @private
|
|
546
|
+
*/
|
|
547
|
+
private handleConnection;
|
|
548
|
+
/**
|
|
549
|
+
* Setup event handlers for a client socket
|
|
550
|
+
*
|
|
551
|
+
* Registers disconnect handler and all custom event handlers for this client.
|
|
552
|
+
* Called once when client connects.
|
|
553
|
+
*
|
|
554
|
+
* @param socket - Socket.IO socket instance
|
|
555
|
+
* @private
|
|
556
|
+
*/
|
|
557
|
+
private setupClientHandlers;
|
|
558
|
+
/**
|
|
559
|
+
* Debug logging utility
|
|
560
|
+
*
|
|
561
|
+
* Logs messages when debug mode is enabled. Uses console.warn
|
|
562
|
+
* to ensure visibility in production environments.
|
|
563
|
+
*
|
|
564
|
+
* @param message - Log message
|
|
565
|
+
* @param data - Optional data to log
|
|
566
|
+
* @private
|
|
567
|
+
*/
|
|
568
|
+
private log;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export { SocketIOServer, SocketIOServer as SocketIOServerType };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var c=Object.defineProperty;var v=(s,e,t)=>e in s?c(s,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[e]=t;var h=(s,e)=>c(s,"name",{value:e,configurable:!0});var r=(s,e,t)=>(v(s,typeof e!="symbol"?e+"":e,t),t);import{Server as p}from"socket.io";import{createServer as u}from"http";var l=class l{constructor(e){r(this,"io",null);r(this,"httpServer",null);r(this,"options");r(this,"running",!1);r(this,"clients",new Map);r(this,"clientData",new Map);r(this,"connectionHandlers",[]);r(this,"disconnectionHandlers",[]);r(this,"eventHandlers",new Map);r(this,"stats",{totalConnections:0,startTime:0});if(!Number.isInteger(e.port)||e.port<0||e.port>65535)throw new Error(`SocketIOServer: Invalid port ${e.port} - must be an integer between 0 and 65535`);if(e.maxConnections!==void 0&&(!Number.isInteger(e.maxConnections)||e.maxConnections<=0))throw new Error(`SocketIOServer: Invalid maxConnections ${e.maxConnections} - must be a positive integer`);if(e.pingInterval!==void 0&&(!Number.isFinite(e.pingInterval)||e.pingInterval<=0))throw new Error(`SocketIOServer: Invalid pingInterval ${e.pingInterval} - must be a positive number`);if(e.pingTimeout!==void 0&&(!Number.isFinite(e.pingTimeout)||e.pingTimeout<=0))throw new Error(`SocketIOServer: Invalid pingTimeout ${e.pingTimeout} - must be a positive number`);this.options={port:e.port,host:e.host??"0.0.0.0",cors:e.cors??{origin:"*"},maxConnections:e.maxConnections??1e3,pingInterval:e.pingInterval??25e3,pingTimeout:e.pingTimeout??5e3,debug:e.debug??!1},this.log("Server initialized",{port:this.options.port,host:this.options.host,maxConnections:this.options.maxConnections})}isRunning(){return this.running}async start(){if(this.running){this.log("Server already running");return}return this.log(`Starting server on ${this.options.host}:${this.options.port}...`),new Promise((e,t)=>{try{this.httpServer=u(),this.io=new p(this.httpServer,{cors:this.options.cors,pingInterval:this.options.pingInterval,pingTimeout:this.options.pingTimeout,maxHttpBufferSize:1e6}),this.io.on("connection",n=>{this.handleConnection(n)}),this.httpServer.listen(this.options.port,this.options.host,()=>{this.running=!0,this.stats.startTime=Date.now(),this.log(`Server started on ${this.options.host}:${this.options.port}`),e()}),this.httpServer.on("error",n=>{this.log(`Server error: ${n.message}`),t(n)})}catch(n){t(n)}})}async stop(){if(this.running)return this.log("Stopping server..."),new Promise(e=>{this.clients.forEach(t=>{t.disconnect(!0)}),this.clients.clear(),this.clientData.clear(),this.io&&(this.io.close(()=>{this.log("Socket.IO server closed")}),this.io=null),this.httpServer?(this.httpServer.close(()=>{this.log("HTTP server closed"),this.running=!1,e()}),this.httpServer=null):(this.running=!1,e())})}getClients(){return Array.from(this.clients.keys())}getClientInfo(e){let t=this.clients.get(e);return t?{id:e,connectedAt:t.connectedAt||Date.now(),address:t.handshake.address,data:this.clientData.get(e)||{}}:null}sendToClient(e,t,n){let i=this.clients.get(e);if(!i){this.log(`Cannot send to client ${e}: not found`);return}i.emit(t,n)}sendToClientVolatile(e,t,n){let i=this.clients.get(e);if(!i){this.log(`Cannot send volatile to client ${e}: not found`);return}i.compress(!1).emit(t,n)}broadcast(e,t){this.io&&(this.io.emit(e,t),this.log(`Broadcast '${e}' to all clients`))}broadcastVolatile(e,t){this.io&&(this.io.volatile.emit(e,t),this.log(`Broadcast volatile '${e}' to all clients`))}broadcastExcept(e,t,n){let i=this.clients.get(e);i&&(i.broadcast.emit(t,n),this.log(`Broadcast '${t}' except ${e}`))}sendToRoom(e,t,n){this.io&&(this.io.to(e).emit(t,n),this.log(`Sent '${t}' to room '${e}'`))}joinRoom(e,t){let n=this.clients.get(e);n&&(n.join(t),this.log(`Client ${e} joined room '${t}'`))}leaveRoom(e,t){let n=this.clients.get(e);n&&(n.leave(t),this.log(`Client ${e} left room '${t}'`))}getRoomClients(e){if(!this.io)return[];let t=this.io.sockets.adapter.rooms.get(e);return t?Array.from(t):[]}disconnectClient(e,t="Server disconnect"){let n=this.clients.get(e);n&&(n.disconnect(!0),this.log(`Disconnected client ${e}: ${t}`))}on(e,t){let n=this.eventHandlers.get(e)||[];n.push(t),this.eventHandlers.set(e,n),this.log(`Registered handler for '${e}'`)}onConnect(e){this.connectionHandlers.push(e),this.log("Registered connection handler")}onDisconnect(e){this.disconnectionHandlers.push(e),this.log("Registered disconnection handler")}off(e,t){let n=this.eventHandlers.get(e);if(!n)return;let i=n.indexOf(t);i!==-1&&(n.splice(i,1),this.log(`Removed handler for '${e}'`))}setClientData(e,t,n){let i=this.clientData.get(e)||{};i[t]=n,this.clientData.set(e,i)}getClientData(e,t){let n=this.clientData.get(e);return n?n[t]:void 0}getStats(){return{connectedClients:this.clients.size,totalConnections:this.stats.totalConnections,uptime:Date.now()-this.stats.startTime}}async destroy(){await this.stop(),this.connectionHandlers=[],this.disconnectionHandlers=[],this.eventHandlers.clear(),this.log("Server destroyed")}handleConnection(e){let t=e.id;if(this.log(`Client connected: ${t}`),this.clients.size>=this.options.maxConnections){this.log(`Max connections reached, rejecting ${t}`),e.emit("error",{code:"MAX_CONNECTIONS",message:"Server is full"}),e.disconnect(!0);return}this.clients.set(t,e),this.clientData.set(t,{}),e.connectedAt=Date.now(),this.stats.totalConnections++,this.setupClientHandlers(e),this.connectionHandlers.forEach(n=>{try{n(t)}catch(i){this.log(`Error in connection handler: ${i}`)}}),e.on("ping",()=>{e.emit("pong")})}setupClientHandlers(e){let t=e.id;e.on("disconnect",n=>{this.log(`Client disconnected: ${t} (${n})`),this.clients.delete(t),this.clientData.delete(t),this.disconnectionHandlers.forEach(i=>{try{i(t,n)}catch(o){this.log(`Error in disconnection handler: ${o}`)}})}),this.eventHandlers.forEach((n,i)=>{e.on(i,o=>{n.forEach(d=>{try{d(t,o)}catch(g){this.log(`Error in event handler for '${i}': ${g}`)}})})})}log(e,t){this.options.debug&&(t!==void 0?console.warn(`[SocketIOServer] ${e}`,t):console.warn(`[SocketIOServer] ${e}`))}};h(l,"SocketIOServer");var a=l;export{a as SocketIOServer};
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@utsp/network-server",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "UTSP Network Server - Server-side communication adapters",
|
|
5
|
+
"author": "Thomas Piquet",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.mjs",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"require": "./dist/index.cjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/thp-software/utsp.git",
|
|
21
|
+
"directory": "packages/network-server"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/thp-software/utsp/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/thp-software/utsp/tree/master/packages/network-server#readme",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"utsp",
|
|
29
|
+
"network",
|
|
30
|
+
"server",
|
|
31
|
+
"websocket",
|
|
32
|
+
"socket.io",
|
|
33
|
+
"communication",
|
|
34
|
+
"multi-user",
|
|
35
|
+
"real-time",
|
|
36
|
+
"terminal",
|
|
37
|
+
"nodejs"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"sideEffects": false,
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE"
|
|
47
|
+
],
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"socket.io": "^4.7.2",
|
|
53
|
+
"@utsp/types": "0.1.1"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^20.0.0",
|
|
57
|
+
"typescript": "^5.6.3"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "node ../../scripts/build-package.mjs packages/network-server",
|
|
61
|
+
"dev": "tsc --watch",
|
|
62
|
+
"clean": "rimraf dist",
|
|
63
|
+
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
|
64
|
+
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
65
|
+
"typecheck": "tsc --noEmit"
|
|
66
|
+
}
|
|
67
|
+
}
|