@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 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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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});
@@ -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
+ }