@utsp/network-client 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/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +310 -0
- package/dist/index.mjs +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Thomas Piquet
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @utsp/network-client
|
|
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
|
+
Client-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-client
|
|
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 c=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var v=Object.prototype.hasOwnProperty;var C=(o,t,e)=>t in o?c(o,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[t]=e;var g=(o,t)=>c(o,"name",{value:t,configurable:!0});var I=(o,t)=>{for(var e in t)c(o,e,{get:t[e],enumerable:!0})},$=(o,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of y(t))!v.call(o,s)&&s!==e&&c(o,s,{get:()=>t[s],enumerable:!(n=w(t,s))||n.enumerable});return o};var R=o=>$(c({},"__esModule",{value:!0}),o);var r=(o,t,e)=>(C(o,typeof t!="symbol"?t+"":t,e),e);var b={};I(b,{NetworkState:()=>d.NetworkState,SocketIOClient:()=>a});module.exports=R(b);var d=require("@utsp/types");var u=require("socket.io-client"),i=require("@utsp/types");var m=class m{constructor(t){r(this,"socket",null);r(this,"options");r(this,"_state",i.NetworkState.Disconnected);r(this,"pingTimestamp",0);r(this,"latency",0);r(this,"pingInterval",null);if(!t.url||typeof t.url!="string")throw new Error("SocketIOClient: Invalid URL - must be a non-empty string");try{if(!new globalThis.URL(t.url).protocol)throw new Error("Invalid protocol")}catch{throw new Error(`SocketIOClient: Invalid URL format - ${t.url}`)}if(t.reconnectDelay!==void 0&&(t.reconnectDelay<0||!Number.isFinite(t.reconnectDelay)))throw new Error(`SocketIOClient: Invalid reconnectDelay - must be a positive number (got ${t.reconnectDelay})`);if(t.maxReconnectAttempts!==void 0&&(!Number.isInteger(t.maxReconnectAttempts)||t.maxReconnectAttempts<0))throw new Error(`SocketIOClient: Invalid maxReconnectAttempts - must be a positive integer (got ${t.maxReconnectAttempts})`);if(t.timeout!==void 0&&(t.timeout<=0||!Number.isFinite(t.timeout)))throw new Error(`SocketIOClient: Invalid timeout - must be a positive number (got ${t.timeout})`);this.options={url:t.url,autoReconnect:t.autoReconnect??!0,reconnectDelay:t.reconnectDelay??1e3,maxReconnectAttempts:t.maxReconnectAttempts??10,timeout:t.timeout??5e3,auth:t.auth??{},debug:t.debug??!1},this.log("Client initialized",{url:this.options.url,autoReconnect:this.options.autoReconnect})}get state(){return this._state}isConnected(){return this._state===i.NetworkState.Connected&&this.socket?.connected===!0}async connect(){if(this.socket&&this.isConnected()){this.log("Already connected");return}return this.log(`Connecting to ${this.options.url}...`),this._state=i.NetworkState.Connecting,new Promise((t,e)=>{this.socket=(0,u.io)(this.options.url,{auth:this.options.auth,reconnection:this.options.autoReconnect,reconnectionDelay:this.options.reconnectDelay,reconnectionAttempts:this.options.maxReconnectAttempts,timeout:this.options.timeout,transports:["websocket"]}),this.socket.on("connect",()=>{this.log(`Connected with ID: ${this.socket?.id}`),this._state=i.NetworkState.Connected,this.startPingMonitor(),t()}),this.socket.on("connect_error",n=>{this.log(`Connection error: ${n.message}`),this._state=i.NetworkState.Error,e(n)}),this.socket.on("disconnect",n=>{this.log(`Disconnected: ${n}`),this._state=i.NetworkState.Disconnected,this.stopPingMonitor()}),this.socket.on("reconnect_attempt",n=>{this.log(`Reconnection attempt ${n}...`),this._state=i.NetworkState.Reconnecting}),this.socket.on("reconnect",n=>{this.log(`Reconnected after ${n} attempts`),this._state=i.NetworkState.Connected,this.startPingMonitor()}),this.socket.on("reconnect_failed",()=>{this.log("Reconnection failed"),this._state=i.NetworkState.Error}),this.socket.on("pong",()=>{this.latency=Date.now()-this.pingTimestamp,this.log(`Ping: ${this.latency}ms`)})})}disconnect(){this.socket&&(this.log("Disconnecting..."),this.stopPingMonitor(),this.socket.disconnect(),this._state=i.NetworkState.Disconnected)}send(t,e){if(!t||typeof t!="string"){this.log(`Cannot send: invalid event name (${typeof t})`);return}if(!this.isConnected()||!this.socket){this.log(`Cannot send '${t}': not connected`);return}try{this.socket.volatile.emit(t,e)}catch(n){this.log(`Error sending '${t}':`,n)}}on(t,e){if(!t||typeof t!="string")throw new Error(`Invalid event name: ${t}`);if(typeof e!="function")throw new Error(`Invalid handler for event '${t}': must be a function`);if(!this.socket){this.log(`Cannot listen to '${t}': socket not initialized`);return}this.socket.on(t,e),this.log(`Listening to '${t}'`)}off(t,e){this.socket&&(this.socket.off(t,e),this.log(`Stopped listening to '${t}'`))}removeAllListeners(t){this.socket&&(t?(this.socket.removeAllListeners(t),this.log(`Removed all listeners for '${t}'`)):(this.socket.removeAllListeners(),this.log("Removed all listeners")))}async request(t,e,n=5e3){if(!t||typeof t!="string")throw new Error(`Invalid event name: ${t}`);if(!Number.isFinite(n)||n<=0)throw new Error(`Invalid timeout: ${n} (must be a positive number)`);if(!this.isConnected())throw new Error("Cannot send request: not connected to server");return new Promise((s,p)=>{let l=`${t}_response`,f=setTimeout(()=>{this.off(l,h),p(new Error(`Request timeout after ${n}ms: ${t}`))},n),h=g(k=>{clearTimeout(f),this.off(l,h),s(k)},"responseHandler");this.on(l,h),this.send(t,e),this.log(`Request sent: ${t} (timeout: ${n}ms)`)})}getPing(){return this.latency}destroy(){this.log("Destroying client..."),this.stopPingMonitor(),this.socket&&(this.socket.removeAllListeners(),this.socket.disconnect(),this.socket=null),this._state=i.NetworkState.Disconnected,this.log("Client destroyed")}startPingMonitor(){this.pingInterval||(this.pingInterval=setInterval(()=>{this.isConnected()&&this.socket&&(this.pingTimestamp=Date.now(),this.socket.emit("ping"))},2e3),this.log("Ping monitor started"))}stopPingMonitor(){this.pingInterval&&(clearInterval(this.pingInterval),this.pingInterval=null,this.log("Ping monitor stopped"))}log(t,e){this.options.debug&&(e!==void 0?console.warn(`[SocketIOClient] ${t}`,e):console.warn(`[SocketIOClient] ${t}`))}};g(m,"SocketIOClient");var a=m;0&&(module.exports={NetworkState,SocketIOClient});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { INetworkClient, NetworkClientOptions, NetworkState, NetworkEventHandler } from '@utsp/types';
|
|
2
|
+
export { AnyNetworkMessage, ChatMessage, ClientInfo, ErrorMessage, INetworkClient, InputMessage, JoinMessage, JoinResponseMessage, LeaveMessage, LoadMessage, MessageType, NetworkClientOptions, NetworkEventHandler, NetworkMessage, NetworkState, PingMessage, PongMessage, UpdateMessage } from '@utsp/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Socket.IO Client Implementation
|
|
6
|
+
*
|
|
7
|
+
* Production-ready Socket.IO client for real-time communication with UTSP servers.
|
|
8
|
+
* Implements the INetworkClient interface with automatic reconnection, latency monitoring,
|
|
9
|
+
* and comprehensive error handling. Browser-safe with no Node.js dependencies.
|
|
10
|
+
*
|
|
11
|
+
* @example Basic Usage
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { SocketIOClient } from '@utsp/network-client';
|
|
14
|
+
*
|
|
15
|
+
* const client = new SocketIOClient({
|
|
16
|
+
* url: 'http://localhost:3000',
|
|
17
|
+
* autoReconnect: true,
|
|
18
|
+
* debug: false
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* await client.connect();
|
|
22
|
+
* console.log('Connected:', client.isConnected());
|
|
23
|
+
*
|
|
24
|
+
* client.on('update', (data) => {
|
|
25
|
+
* console.log('Received update:', data);
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* client.send('input', { key: 'ArrowUp' });
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @example With Authentication
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const client = new SocketIOClient({
|
|
34
|
+
* url: 'https://game.example.com',
|
|
35
|
+
* auth: {
|
|
36
|
+
* token: 'user-auth-token',
|
|
37
|
+
* userId: '12345'
|
|
38
|
+
* }
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* await client.connect();
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @example Request-Response Pattern
|
|
45
|
+
* ```typescript
|
|
46
|
+
* try {
|
|
47
|
+
* const response = await client.request('getPlayerStats', { playerId: '123' }, 5000);
|
|
48
|
+
* console.log('Stats:', response);
|
|
49
|
+
* } catch (error) {
|
|
50
|
+
* console.error('Request failed:', error);
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Socket.IO client implementation for UTSP network communication
|
|
57
|
+
*
|
|
58
|
+
* Features:
|
|
59
|
+
* - Automatic reconnection with exponential backoff
|
|
60
|
+
* - Real-time latency monitoring (ping/pong)
|
|
61
|
+
* - Request-response pattern with timeout
|
|
62
|
+
* - Type-safe event handling
|
|
63
|
+
* - Connection state management
|
|
64
|
+
* - Debug logging
|
|
65
|
+
*
|
|
66
|
+
* @implements {INetworkClient}
|
|
67
|
+
*/
|
|
68
|
+
declare class SocketIOClient implements INetworkClient {
|
|
69
|
+
private socket;
|
|
70
|
+
private options;
|
|
71
|
+
private _state;
|
|
72
|
+
private pingTimestamp;
|
|
73
|
+
private latency;
|
|
74
|
+
private pingInterval;
|
|
75
|
+
/**
|
|
76
|
+
* Create a new Socket.IO client instance
|
|
77
|
+
*
|
|
78
|
+
* @param options - Client configuration options
|
|
79
|
+
* @throws {Error} If URL is invalid or options are malformed
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* const client = new SocketIOClient({
|
|
84
|
+
* url: 'http://localhost:3000',
|
|
85
|
+
* autoReconnect: true,
|
|
86
|
+
* reconnectDelay: 1000,
|
|
87
|
+
* maxReconnectAttempts: 10,
|
|
88
|
+
* timeout: 5000,
|
|
89
|
+
* debug: false
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
constructor(options: NetworkClientOptions);
|
|
94
|
+
/**
|
|
95
|
+
* Get current connection state
|
|
96
|
+
*
|
|
97
|
+
* @returns Current network state (Disconnected, Connecting, Connected, Reconnecting, Error)
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* console.log('State:', client.state); // NetworkState.Connected
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
get state(): NetworkState;
|
|
105
|
+
/**
|
|
106
|
+
* Check if client is currently connected
|
|
107
|
+
*
|
|
108
|
+
* @returns true if connected and socket is active, false otherwise
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* if (client.isConnected()) {
|
|
113
|
+
* client.send('message', data);
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
isConnected(): boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Connect to the server
|
|
120
|
+
*
|
|
121
|
+
* Establishes a WebSocket connection to the configured server URL.
|
|
122
|
+
* Automatically sets up reconnection logic if enabled.
|
|
123
|
+
*
|
|
124
|
+
* @returns Promise that resolves when connected, rejects on connection error
|
|
125
|
+
* @throws {Error} If connection fails or times out
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* try {
|
|
130
|
+
* await client.connect();
|
|
131
|
+
* console.log('Connected successfully');
|
|
132
|
+
* } catch (error) {
|
|
133
|
+
* console.error('Connection failed:', error);
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
connect(): Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* Disconnect from the server
|
|
140
|
+
*
|
|
141
|
+
* Gracefully closes the WebSocket connection and stops all monitoring.
|
|
142
|
+
* Safe to call multiple times.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* client.disconnect();
|
|
147
|
+
* console.log('Disconnected');
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
disconnect(): void;
|
|
151
|
+
/**
|
|
152
|
+
* Send an event to the server
|
|
153
|
+
*
|
|
154
|
+
* Uses volatile emit to prevent message queuing during disconnection.
|
|
155
|
+
* Events sent while disconnected will be silently dropped.
|
|
156
|
+
*
|
|
157
|
+
* @param event - Event name to emit
|
|
158
|
+
* @param data - Event payload (any serializable data)
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```typescript
|
|
162
|
+
* client.send('input', { key: 'ArrowUp', timestamp: Date.now() });
|
|
163
|
+
* client.send('chat', { message: 'Hello world!' });
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
send(event: string, data: any): void;
|
|
167
|
+
/**
|
|
168
|
+
* Register an event listener
|
|
169
|
+
*
|
|
170
|
+
* Listens for events from the server. Multiple handlers can be registered
|
|
171
|
+
* for the same event.
|
|
172
|
+
*
|
|
173
|
+
* @param event - Event name to listen for
|
|
174
|
+
* @param handler - Callback function to handle event data
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```typescript
|
|
178
|
+
* client.on<UpdateMessage>('update', (data) => {
|
|
179
|
+
* console.log('Game update:', data);
|
|
180
|
+
* });
|
|
181
|
+
*
|
|
182
|
+
* client.on('chat', (message) => {
|
|
183
|
+
* console.log('Chat:', message);
|
|
184
|
+
* });
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
on<T = any>(event: string, handler: NetworkEventHandler<T>): void;
|
|
188
|
+
/**
|
|
189
|
+
* Unregister a specific event listener
|
|
190
|
+
*
|
|
191
|
+
* Removes a previously registered handler for an event. The handler reference
|
|
192
|
+
* must be the exact same function object that was registered.
|
|
193
|
+
*
|
|
194
|
+
* @param event - Event name
|
|
195
|
+
* @param handler - Handler function to remove (must be same reference)
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* const handler = (data) => console.log(data);
|
|
200
|
+
* client.on('update', handler);
|
|
201
|
+
* // ... later
|
|
202
|
+
* client.off('update', handler);
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
off<T = any>(event: string, handler: NetworkEventHandler<T>): void;
|
|
206
|
+
/**
|
|
207
|
+
* Remove all listeners for an event or all events
|
|
208
|
+
*
|
|
209
|
+
* If event name is provided, removes all listeners for that specific event.
|
|
210
|
+
* If no event name, removes all listeners for all events.
|
|
211
|
+
*
|
|
212
|
+
* @param event - Optional event name. If omitted, removes all listeners.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```typescript
|
|
216
|
+
* // Remove all 'update' listeners
|
|
217
|
+
* client.removeAllListeners('update');
|
|
218
|
+
*
|
|
219
|
+
* // Remove ALL listeners
|
|
220
|
+
* client.removeAllListeners();
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
removeAllListeners(event?: string): void;
|
|
224
|
+
/**
|
|
225
|
+
* Send a request and wait for response (request-response pattern)
|
|
226
|
+
*
|
|
227
|
+
* Implements a request-response pattern on top of Socket.IO events.
|
|
228
|
+
* Automatically listens for `${event}_response` and handles timeout.
|
|
229
|
+
*
|
|
230
|
+
* @param event - Request event name
|
|
231
|
+
* @param data - Request payload
|
|
232
|
+
* @param timeout - Timeout in milliseconds (default: 5000ms)
|
|
233
|
+
* @returns Promise resolving to the response data
|
|
234
|
+
* @throws {Error} If not connected, request times out, or server returns error
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* try {
|
|
239
|
+
* const stats = await client.request<PlayerStats>(
|
|
240
|
+
* 'getPlayerStats',
|
|
241
|
+
* { playerId: '123' },
|
|
242
|
+
* 3000
|
|
243
|
+
* );
|
|
244
|
+
* console.log('Stats:', stats);
|
|
245
|
+
* } catch (error) {
|
|
246
|
+
* console.error('Request failed:', error);
|
|
247
|
+
* }
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
request<T = any>(event: string, data: any, timeout?: number): Promise<T>;
|
|
251
|
+
/**
|
|
252
|
+
* Get current network latency (ping time)
|
|
253
|
+
*
|
|
254
|
+
* Returns the round-trip time in milliseconds of the most recent ping.
|
|
255
|
+
* Updated every 2 seconds while connected.
|
|
256
|
+
*
|
|
257
|
+
* @returns Latency in milliseconds, or 0 if not yet measured
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```typescript
|
|
261
|
+
* console.log('Latency:', client.getPing(), 'ms');
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
getPing(): number;
|
|
265
|
+
/**
|
|
266
|
+
* Destroy the client and clean up all resources
|
|
267
|
+
*
|
|
268
|
+
* Disconnects from server, removes all listeners, and releases resources.
|
|
269
|
+
* Client instance cannot be reused after calling destroy().
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```typescript
|
|
273
|
+
* // Cleanup before page unload
|
|
274
|
+
* window.addEventListener('beforeunload', () => {
|
|
275
|
+
* client.destroy();
|
|
276
|
+
* });
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
destroy(): void;
|
|
280
|
+
/**
|
|
281
|
+
* Start ping monitoring for latency measurement
|
|
282
|
+
*
|
|
283
|
+
* Sends a ping every 2 seconds to measure round-trip time.
|
|
284
|
+
* Only runs while connected.
|
|
285
|
+
*
|
|
286
|
+
* @private
|
|
287
|
+
*/
|
|
288
|
+
private startPingMonitor;
|
|
289
|
+
/**
|
|
290
|
+
* Stop ping monitoring
|
|
291
|
+
*
|
|
292
|
+
* Clears the ping interval timer.
|
|
293
|
+
*
|
|
294
|
+
* @private
|
|
295
|
+
*/
|
|
296
|
+
private stopPingMonitor;
|
|
297
|
+
/**
|
|
298
|
+
* Debug logging utility
|
|
299
|
+
*
|
|
300
|
+
* Logs messages when debug mode is enabled. Uses console.warn
|
|
301
|
+
* to ensure visibility in production environments.
|
|
302
|
+
*
|
|
303
|
+
* @param message - Log message
|
|
304
|
+
* @param data - Optional data to log
|
|
305
|
+
* @private
|
|
306
|
+
*/
|
|
307
|
+
private log;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export { SocketIOClient, SocketIOClient as SocketIOClientType };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var g=Object.defineProperty;var f=(i,t,e)=>t in i?g(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var a=(i,t)=>g(i,"name",{value:t,configurable:!0});var s=(i,t,e)=>(f(i,typeof t!="symbol"?t+"":t,e),e);import{NetworkState as E}from"@utsp/types";import{io as k}from"socket.io-client";import{NetworkState as o}from"@utsp/types";var h=class h{constructor(t){s(this,"socket",null);s(this,"options");s(this,"_state",o.Disconnected);s(this,"pingTimestamp",0);s(this,"latency",0);s(this,"pingInterval",null);if(!t.url||typeof t.url!="string")throw new Error("SocketIOClient: Invalid URL - must be a non-empty string");try{if(!new globalThis.URL(t.url).protocol)throw new Error("Invalid protocol")}catch{throw new Error(`SocketIOClient: Invalid URL format - ${t.url}`)}if(t.reconnectDelay!==void 0&&(t.reconnectDelay<0||!Number.isFinite(t.reconnectDelay)))throw new Error(`SocketIOClient: Invalid reconnectDelay - must be a positive number (got ${t.reconnectDelay})`);if(t.maxReconnectAttempts!==void 0&&(!Number.isInteger(t.maxReconnectAttempts)||t.maxReconnectAttempts<0))throw new Error(`SocketIOClient: Invalid maxReconnectAttempts - must be a positive integer (got ${t.maxReconnectAttempts})`);if(t.timeout!==void 0&&(t.timeout<=0||!Number.isFinite(t.timeout)))throw new Error(`SocketIOClient: Invalid timeout - must be a positive number (got ${t.timeout})`);this.options={url:t.url,autoReconnect:t.autoReconnect??!0,reconnectDelay:t.reconnectDelay??1e3,maxReconnectAttempts:t.maxReconnectAttempts??10,timeout:t.timeout??5e3,auth:t.auth??{},debug:t.debug??!1},this.log("Client initialized",{url:this.options.url,autoReconnect:this.options.autoReconnect})}get state(){return this._state}isConnected(){return this._state===o.Connected&&this.socket?.connected===!0}async connect(){if(this.socket&&this.isConnected()){this.log("Already connected");return}return this.log(`Connecting to ${this.options.url}...`),this._state=o.Connecting,new Promise((t,e)=>{this.socket=k(this.options.url,{auth:this.options.auth,reconnection:this.options.autoReconnect,reconnectionDelay:this.options.reconnectDelay,reconnectionAttempts:this.options.maxReconnectAttempts,timeout:this.options.timeout,transports:["websocket"]}),this.socket.on("connect",()=>{this.log(`Connected with ID: ${this.socket?.id}`),this._state=o.Connected,this.startPingMonitor(),t()}),this.socket.on("connect_error",n=>{this.log(`Connection error: ${n.message}`),this._state=o.Error,e(n)}),this.socket.on("disconnect",n=>{this.log(`Disconnected: ${n}`),this._state=o.Disconnected,this.stopPingMonitor()}),this.socket.on("reconnect_attempt",n=>{this.log(`Reconnection attempt ${n}...`),this._state=o.Reconnecting}),this.socket.on("reconnect",n=>{this.log(`Reconnected after ${n} attempts`),this._state=o.Connected,this.startPingMonitor()}),this.socket.on("reconnect_failed",()=>{this.log("Reconnection failed"),this._state=o.Error}),this.socket.on("pong",()=>{this.latency=Date.now()-this.pingTimestamp,this.log(`Ping: ${this.latency}ms`)})})}disconnect(){this.socket&&(this.log("Disconnecting..."),this.stopPingMonitor(),this.socket.disconnect(),this._state=o.Disconnected)}send(t,e){if(!t||typeof t!="string"){this.log(`Cannot send: invalid event name (${typeof t})`);return}if(!this.isConnected()||!this.socket){this.log(`Cannot send '${t}': not connected`);return}try{this.socket.volatile.emit(t,e)}catch(n){this.log(`Error sending '${t}':`,n)}}on(t,e){if(!t||typeof t!="string")throw new Error(`Invalid event name: ${t}`);if(typeof e!="function")throw new Error(`Invalid handler for event '${t}': must be a function`);if(!this.socket){this.log(`Cannot listen to '${t}': socket not initialized`);return}this.socket.on(t,e),this.log(`Listening to '${t}'`)}off(t,e){this.socket&&(this.socket.off(t,e),this.log(`Stopped listening to '${t}'`))}removeAllListeners(t){this.socket&&(t?(this.socket.removeAllListeners(t),this.log(`Removed all listeners for '${t}'`)):(this.socket.removeAllListeners(),this.log("Removed all listeners")))}async request(t,e,n=5e3){if(!t||typeof t!="string")throw new Error(`Invalid event name: ${t}`);if(!Number.isFinite(n)||n<=0)throw new Error(`Invalid timeout: ${n} (must be a positive number)`);if(!this.isConnected())throw new Error("Cannot send request: not connected to server");return new Promise((m,u)=>{let r=`${t}_response`,d=setTimeout(()=>{this.off(r,c),u(new Error(`Request timeout after ${n}ms: ${t}`))},n),c=a(p=>{clearTimeout(d),this.off(r,c),m(p)},"responseHandler");this.on(r,c),this.send(t,e),this.log(`Request sent: ${t} (timeout: ${n}ms)`)})}getPing(){return this.latency}destroy(){this.log("Destroying client..."),this.stopPingMonitor(),this.socket&&(this.socket.removeAllListeners(),this.socket.disconnect(),this.socket=null),this._state=o.Disconnected,this.log("Client destroyed")}startPingMonitor(){this.pingInterval||(this.pingInterval=setInterval(()=>{this.isConnected()&&this.socket&&(this.pingTimestamp=Date.now(),this.socket.emit("ping"))},2e3),this.log("Ping monitor started"))}stopPingMonitor(){this.pingInterval&&(clearInterval(this.pingInterval),this.pingInterval=null,this.log("Ping monitor stopped"))}log(t,e){this.options.debug&&(e!==void 0?console.warn(`[SocketIOClient] ${t}`,e):console.warn(`[SocketIOClient] ${t}`))}};a(h,"SocketIOClient");var l=h;export{E as NetworkState,l as SocketIOClient};
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@utsp/network-client",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "UTSP Network Client - Client-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-client"
|
|
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-client#readme",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"utsp",
|
|
29
|
+
"network",
|
|
30
|
+
"client",
|
|
31
|
+
"websocket",
|
|
32
|
+
"socket.io",
|
|
33
|
+
"communication",
|
|
34
|
+
"multi-user",
|
|
35
|
+
"real-time",
|
|
36
|
+
"terminal"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"socket.io-client": "^4.7.2",
|
|
52
|
+
"@utsp/types": "0.1.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"typescript": "^5.6.3"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "node ../../scripts/build-package.mjs packages/network-client",
|
|
59
|
+
"dev": "tsc --watch",
|
|
60
|
+
"clean": "rimraf dist",
|
|
61
|
+
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
|
62
|
+
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
63
|
+
"typecheck": "tsc --noEmit"
|
|
64
|
+
}
|
|
65
|
+
}
|