@utsp/network-client 0.17.3 → 0.18.0-nightly.20260204223142.cdfe4ca
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 +23 -19
- package/dist/index.cjs +3 -3
- package/dist/index.d.ts +9 -0
- package/dist/index.mjs +3 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,36 +1,40 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# @utsp/network-client
|
|
2
4
|
|
|
3
|
-
|
|
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.
|
|
5
|
+
[](https://www.npmjs.com/package/@utsp/network-client)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Visit [**UTSP.dev**](https://utsp.dev/) for more information.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
The **Client-side networking** layer for UTSP. It provides standardized transport implementations for both Socket.IO and WebRTC, enabling seamless connectivity between high-level runtimes and remote servers.
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
> [!WARNING]
|
|
17
|
+
> **PROTOTYPE - NOT READY FOR PRODUCTION**
|
|
18
|
+
> This package is under active development. The API is unstable and subject to breaking changes.
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
> [!NOTE]
|
|
21
|
+
> **Source Code Availability**
|
|
22
|
+
> While this package is licensed under MIT, the source code is currently being finalized for public release. It will be available on GitHub in the coming months.
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
- ❌ No documentation available yet
|
|
18
|
-
- ❌ Breaking changes expected
|
|
19
|
-
- ❌ Not recommended for production use
|
|
24
|
+
## ✨ Features
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
- **🔌 Unified Transport**: Standardized API for Socket.IO and WebRTC.
|
|
22
27
|
|
|
23
|
-
## Installation
|
|
28
|
+
## 📦 Installation
|
|
24
29
|
|
|
25
30
|
```bash
|
|
26
31
|
npm install @utsp/network-client
|
|
27
32
|
```
|
|
28
33
|
|
|
29
|
-
##
|
|
34
|
+
## 📖 Documentation
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
- [Issues](https://github.com/thp-software/utsp/issues)
|
|
36
|
+
For detailed guides on the protocol and application development, visit the official [**Documentation**](https://docs.utsp.dev/introduction).
|
|
33
37
|
|
|
34
|
-
## License
|
|
38
|
+
## 📄 License
|
|
35
39
|
|
|
36
|
-
MIT ©
|
|
40
|
+
MIT © 2026 [THP Software](https://github.com/thp-software)
|
package/dist/index.cjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";var p=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var T=Object.getOwnPropertyNames;var A=Object.prototype.hasOwnProperty;var P=(a,e,t)=>e in a?p(a,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):a[e]=t;var u=(a,e)=>p(a,"name",{value:e,configurable:!0});var E=(a,e)=>{for(var t in e)p(a,t,{get:e[t],enumerable:!0})},$=(a,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of T(e))!A.call(a,i)&&i!==t&&p(a,i,{get:()=>e[i],enumerable:!(n=R(e,i))||n.enumerable});return a};var N=a=>$(p({},"__esModule",{value:!0}),a);var o=(a,e,t)=>(P(a,typeof e!="symbol"?e+"":e,t),t);var D={};E(D,{NetworkState:()=>I.NetworkState,SocketIOClient:()=>f,WebRTCClient:()=>y});module.exports=N(D);var I=require("@utsp/types");var b=require("socket.io-client"),h=require("@utsp/types");var v=class v{constructor(e){o(this,"socket",null);o(this,"options");o(this,"_state",h.NetworkState.Disconnected);o(this,"pingTimestamp",0);o(this,"latency",0);o(this,"pingInterval",null);o(this,"bridgeHandlers",new Map);o(this,"bridgeSetup",!1);if(!e.url||typeof e.url!="string")throw new Error("SocketIOClient: Invalid URL - must be a non-empty string");try{if(!new globalThis.URL(e.url).protocol)throw new Error("Invalid protocol")}catch{throw new Error(`SocketIOClient: Invalid URL format - ${e.url}`)}if(e.reconnectDelay!==void 0&&(e.reconnectDelay<0||!Number.isFinite(e.reconnectDelay)))throw new Error(`SocketIOClient: Invalid reconnectDelay - must be a positive number (got ${e.reconnectDelay})`);if(e.maxReconnectAttempts!==void 0&&(!Number.isInteger(e.maxReconnectAttempts)||e.maxReconnectAttempts<0))throw new Error(`SocketIOClient: Invalid maxReconnectAttempts - must be a positive integer (got ${e.maxReconnectAttempts})`);if(e.timeout!==void 0&&(e.timeout<=0||!Number.isFinite(e.timeout)))throw new Error(`SocketIOClient: Invalid timeout - must be a positive number (got ${e.timeout})`);this.options={url:e.url,autoReconnect:e.autoReconnect??!0,reconnectDelay:e.reconnectDelay??1e3,maxReconnectAttempts:e.maxReconnectAttempts??10,timeout:e.timeout??5e3,auth:e.auth??{},iceServers:e.iceServers??[],path:e.path,debug:e.debug??!1,sessionId:e.sessionId},this.log("Client initialized",{url:this.options.url,autoReconnect:this.options.autoReconnect})}get state(){return this._state}isConnected(){return this._state===h.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=h.NetworkState.Connecting,this.bridgeSetup=!1,new Promise((e,t)=>{this.socket=(0,b.io)(this.options.url,{path:this.options.path,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=h.NetworkState.Connected,this.startPingMonitor(),this.bridgeHandlers.size>0&&this.setupBridgeListener(),e()}),this.socket.on("connect_error",n=>{this.log(`Connection error: ${n.message}`),this._state=h.NetworkState.Error,t(n)}),this.socket.on("disconnect",n=>{this.log(`Disconnected: ${n}`),this._state=h.NetworkState.Disconnected,this.stopPingMonitor()}),this.socket.on("reconnect_attempt",n=>{this.log(`Reconnection attempt ${n}...`),this._state=h.NetworkState.Reconnecting}),this.socket.on("reconnect",n=>{this.log(`Reconnected after ${n} attempts`),this._state=h.NetworkState.Connected,this.startPingMonitor()}),this.socket.on("reconnect_failed",()=>{this.log("Reconnection failed"),this._state=h.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=h.NetworkState.Disconnected)}send(e,t){if(!e||typeof e!="string"){this.log(`Cannot send: invalid event name (${typeof e})`);return}if(!this.isConnected()||!this.socket){this.log(`Cannot send '${e}': not connected`);return}try{this.socket.emit(e,t)}catch(n){this.log(`Error sending '${e}':`,n)}}sendVolatile(e,t){if(!(!e||typeof e!="string")&&!(!this.isConnected()||!this.socket))try{this.socket.volatile.emit(e,t)}catch(n){this.log(`Error sending volatile '${e}':`,n)}}on(e,t){if(!e||typeof e!="string")throw new Error(`Invalid event name: ${e}`);if(typeof t!="function")throw new Error(`Invalid handler for event '${e}': must be a function`);if(!this.socket){this.log(`Cannot listen to '${e}': socket not initialized`);return}this.socket.on(e,t),this.log(`Listening to '${e}'`)}off(e,t){this.socket&&(this.socket.off(e,t),this.log(`Stopped listening to '${e}'`))}removeAllListeners(e){this.socket&&(e?(this.socket.removeAllListeners(e),this.log(`Removed all listeners for '${e}'`)):(this.socket.removeAllListeners(),this.log("Removed all listeners")))}async request(e,t,n=5e3){if(!e||typeof e!="string")throw new Error(`Invalid event name: ${e}`);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((i,s)=>{let r=`${e}_response`,l=setTimeout(()=>{this.off(r,d),s(new Error(`Request timeout after ${n}ms: ${e}`))},n),d=u(g=>{clearTimeout(l),this.off(r,d),i(g)},"responseHandler");this.on(r,d),this.send(e,t),this.log(`Request sent: ${e} (timeout: ${n}ms)`)})}getPing(){return this.latency}sendBridge(e,t){if(!this.isConnected()||!this.socket){this.log(`Cannot send bridge '${e}': not connected`);return}try{let n={channel:e,data:JSON.stringify(t)};this.socket.emit("bridge",n),this.log(`Bridge sent on channel '${e}'`)}catch(n){this.log(`Error sending bridge: ${n}`)}}onBridge(e,t){this.bridgeHandlers.has(e)||this.bridgeHandlers.set(e,[]),this.bridgeHandlers.get(e).push(t),this.log(`Bridge handler registered for channel '${e}'`),this.setupBridgeListener()}offBridge(e,t){let n=this.bridgeHandlers.get(e);if(n){let i=n.indexOf(t);i!==-1&&(n.splice(i,1),this.log(`Bridge handler removed for channel '${e}'`))}}removeAllBridgeHandlers(e){e?(this.bridgeHandlers.delete(e),this.log(`All bridge handlers removed for channel '${e}'`)):(this.bridgeHandlers.clear(),this.log("All bridge handlers removed"))}setupBridgeListener(){this.bridgeSetup||!this.socket||(this.socket.on("bridge",e=>{this.handleBridgeMessage(e)}),this.bridgeSetup=!0,this.log("Bridge listener setup"))}handleBridgeMessage(e){try{let{channel:t,data:n}=e,i=JSON.parse(n),s=this.bridgeHandlers.get(t);s&&s.length>0?s.forEach(r=>{try{r(i)}catch(l){this.log(`Error in bridge handler for '${t}': ${l}`)}}):this.log(`No bridge handler for channel '${t}'`)}catch(t){this.log(`Error parsing bridge message: ${t}`)}}destroy(){this.log("Destroying client..."),this.stopPingMonitor(),this.bridgeHandlers.clear(),this.bridgeSetup=!1,this.socket&&(this.socket.removeAllListeners(),this.socket.disconnect(),this.socket=null),this._state=h.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(e,t){this.options.debug&&(t!==void 0?console.warn(`[SocketIOClient] ${e}`,t):console.warn(`[SocketIOClient] ${e}`))}};u(v,"SocketIOClient");var f=v;var w=require("socket.io-client"),c=require("@utsp/types");var k=class k{constructor(e){o(this,"signalingSocket",null);o(this,"peerConnection",null);o(this,"reliableChannel",null);o(this,"unreliableChannel",null);o(this,"options");o(this,"_state",c.NetworkState.Disconnected);o(this,"p2pOnly",!0);o(this,"eventHandlers",new Map);o(this,"bridgeHandlers",new Map);o(this,"chunkBuffers",new Map);o(this,"remotePeerId",null);o(this,"latency",0);o(this,"retryCount",0);o(this,"pingTimestamp",0);o(this,"pingInterval",null);o(this,"statsInterval",null);let t=e.sessionId?"https://signal.utsp.dev":void 0,n=e.url||t;if(!n)throw new Error("WebRTCClient: URL required (or sessionId for public relay)");this.options={url:n,autoReconnect:e.autoReconnect??!0,reconnectDelay:e.reconnectDelay??1e3,maxReconnectAttempts:e.maxReconnectAttempts??10,timeout:e.timeout??5e3,auth:e.auth??{},iceServers:e.iceServers??[{urls:"stun:stun.l.google.com:19302"}],debug:e.debug??!1,signalingPath:e.signalingPath??(e.sessionId?"/socket.io":"/utsp-signal"),sessionId:e.sessionId,p2pOnly:e.p2pOnly??!0},this.p2pOnly=this.options.p2pOnly??!0,this.on("bridge",i=>{if(!i||!i.channel)return;let s=i.data;if(typeof s=="string")try{s=JSON.parse(s)}catch{}let r=this.bridgeHandlers.get(i.channel);r&&r.forEach(l=>l(s))})}get state(){return this._state}isConnected(){return this._state===c.NetworkState.Connected}async connect(){if(!(this._state===c.NetworkState.Connected||this._state===c.NetworkState.Connecting))return this._state=c.NetworkState.Connecting,this.log("Connecting via WebRTC..."),new Promise((e,t)=>{this.signalingSocket=(0,w.io)(this.options.url,{path:this.options.signalingPath,auth:this.options.auth,reconnection:this.options.autoReconnect,reconnectionDelay:this.options.reconnectDelay,reconnectionAttempts:this.options.maxReconnectAttempts,transports:["websocket","polling"]}),this.options.sessionId?(this.log(`Connecting via Public Relay to session: ${this.options.sessionId}`),this.signalingSocket.on("connect",()=>{this.log("Connected to Relay"),this.signalingSocket.emit("join-session",this.options.sessionId)}),this.signalingSocket.on("disconnect",i=>{this.warn(`Relay signaled disconnect: ${i}`)}),this.signalingSocket.on("signal",({from:i,type:s,data:r})=>{this.remotePeerId=i,s==="rtc:offer"?(this.log("Received RTC Offer via Relay"),this.handleOffer(r)):s==="rtc:candidate"&&this.peerConnection&&(this.log("Received ICE Candidate via Relay"),this.peerConnection.addIceCandidate(r).catch(l=>this.error("ICE Error",l)))})):(this.signalingSocket.on("connect",()=>{this.log("Signaling connected")}),this.signalingSocket.on("rtc:offer",async i=>{this.log("Received RTC Offer"),await this.handleOffer(i)}),this.signalingSocket.on("rtc:candidate",i=>{let s=this.parseCandidateType(i.candidate);if(this.p2pOnly&&s==="relay"){this.warn("Strict P2P: Dropping received RELAY candidate");return}this.log(`Received ICE Candidate (${s})`),this.peerConnection?.addIceCandidate(new RTCIceCandidate(i))})),this.signalingSocket.on("connect_error",i=>{this.error("Signaling connection error",i),this.signalingSocket&&!this.signalingSocket.active&&(this._state=c.NetworkState.Error,t(i))}),this.signalingSocket.on("disconnect",()=>{this.log("Signaling disconnected"),this._state===c.NetworkState.Connecting&&this.disconnect()}),this.signalingSocket.onAny((i,...s)=>{i.startsWith("rtc:")||i==="signal"||this.handleEvent(i,s[0])});let n=setTimeout(()=>{this._state!==c.NetworkState.Connected&&(this.disconnect(),t(new Error("Connection timeout")))},this.options.timeout);this._connectResolve=()=>{clearTimeout(n),e()}})}async handleOffer(e){this.peerConnection&&this.peerConnection.close(),this.peerConnection=new RTCPeerConnection({iceServers:this.options.iceServers}),this.setupPeerEvents();try{let t=this.p2pOnly?this.stripRelayFromSDP(e.sdp||""):e.sdp;await this.peerConnection.setRemoteDescription(new RTCSessionDescription({type:"offer",sdp:t}));let n=await this.peerConnection.createAnswer(),i=this.p2pOnly?this.stripRelayFromSDP(n.sdp||""):n.sdp||"";await this.peerConnection.setLocalDescription({type:"answer",sdp:i}),this.options.sessionId&&this.remotePeerId?this.signalingSocket?.emit("signal",{target:this.remotePeerId,type:"rtc:answer",data:n}):(this.log("Sending RTC Answer"),this.signalingSocket?.emit("rtc:answer",n))}catch(t){this.error("WebRTC Negotiation failed",t),this.disconnect()}}setupPeerEvents(){this.peerConnection&&(this.peerConnection.onicecandidate=e=>{if(e.candidate)if(this.options.sessionId&&this.remotePeerId)this.signalingSocket?.emit("signal",{target:this.remotePeerId,type:"rtc:candidate",data:e.candidate});else{let t=this.parseCandidateType(e.candidate.candidate);if(this.p2pOnly&&t==="relay"){this.warn("Strict P2P: Not sending RELAY candidate");return}this.log(`Sending ICE Candidate (${t})`),this.signalingSocket?.emit("rtc:candidate",e.candidate)}},this.peerConnection.ondatachannel=e=>{let t=e.channel;this.log(`DataChannel received: ${t.label}`),t.label==="reliable"?(this.reliableChannel=t,this.setupChannel(t),t.onopen=()=>{this.log("Reliable Channel OPEN"),this._state=c.NetworkState.Connected,this.startPingMonitor(),this.startStatsMonitor(),this._connectResolve&&this._connectResolve()}):t.label==="unreliable"&&(this.log("Unreliable Channel RECEIVED"),this.unreliableChannel=t,this.setupChannel(t))},this.peerConnection.onconnectionstatechange=()=>{this.log(`Connection State: ${this.peerConnection?.connectionState}`);let e=this.peerConnection?.connectionState;e==="disconnected"||e==="failed"?this.options.autoReconnect&&this.retryCount<this.options.maxReconnectAttempts?(this.log(`WebRTC lost. Retrying in ${this.options.reconnectDelay}ms... (Attempt ${this.retryCount+1}/${this.options.maxReconnectAttempts})`),this.peerConnection&&(this.peerConnection.close(),this.peerConnection=null),this.retryCount++,setTimeout(()=>{this._state=c.NetworkState.Disconnected,this.connect().catch(t=>this.error("Reconnection failed",t))},this.options.reconnectDelay*Math.pow(1.5,this.retryCount))):this.disconnect():e==="connected"&&(this.retryCount=0)})}parsePayload(e){return JSON.parse(e,(t,n)=>{if(n&&typeof n=="object"&&n._b64){let i=atob(n._b64),s=new Uint8Array(i.length);for(let r=0;r<i.length;r++)s[r]=i.charCodeAt(r);return s}return n})}setupChannel(e){e.binaryType="arraybuffer",e.onmessage=t=>{let n=t.data;if(e.label,n instanceof ArrayBuffer){try{let i=new DataView(n),s=i.getUint8(0);if(s===255){let C=i.getUint8(1),S=new Uint8Array(n,2);this.handleEvent(C,S);return}let r=s,l=new Uint8Array(n,1,r),g=new TextDecoder("utf-8").decode(l),m=new Uint8Array(n,1+r);this.handleEvent(g,m)}catch(i){this.error("Failed to parse binary packet",i)}return}try{let[i,s]=this.parsePayload(n);this.handleEvent(i,s)}catch{}}}disconnect(){this._state=c.NetworkState.Disconnected,this.reliableChannel&&this.reliableChannel.close(),this.unreliableChannel&&this.unreliableChannel.close(),this.peerConnection&&this.peerConnection.close(),this.signalingSocket&&this.signalingSocket.disconnect(),this.stopPingMonitor(),this.stopStatsMonitor(),this.reliableChannel=null,this.unreliableChannel=null,this.peerConnection=null,this.signalingSocket=null}preparePayload(e,t){let n=t;return t instanceof Uint8Array||t instanceof Int8Array||t instanceof Uint16Array||t instanceof Int16Array||t instanceof Uint32Array||t instanceof Int32Array||t instanceof Float32Array||t instanceof Float64Array?n=Array.from(t):t instanceof ArrayBuffer&&(n=Array.from(new Uint8Array(t))),JSON.stringify([e,n])}send(e,t){if(this.reliableChannel?.readyState==="open"){if(typeof e=="number"&&(t instanceof Uint8Array||t instanceof ArrayBuffer)){let i=new Uint8Array(2);i[0]=255,i[1]=e;let s=t instanceof ArrayBuffer?new Uint8Array(t):t,r=new Uint8Array(i.length+s.length);r.set(i),r.set(s,i.length),this.reliableChannel.send(r);return}if(typeof e=="number"){let i=this.preparePayload(String(e),t);this.reliableChannel.send(i);return}let n=this.preparePayload(e,t);this.reliableChannel.send(n)}}sendVolatile(e,t){let n=this.unreliableChannel?.readyState==="open"?this.unreliableChannel:this.reliableChannel;if(n?.readyState==="open"){if(n.bufferedAmount>16*1024){this.warn(`Backpressure: Dropping volatile packet on ${n.label} (buffered: ${n.bufferedAmount} bytes)`);return}if(typeof e=="number"&&(t instanceof Uint8Array||t instanceof ArrayBuffer)){let s=new Uint8Array(2);s[0]=255,s[1]=e;let r=t instanceof ArrayBuffer?new Uint8Array(t):t,l=new Uint8Array(s.length+r.length);l.set(s),l.set(r,s.length),n.send(l);return}let i=this.preparePayload(String(e),t);n.send(i)}}request(e,t,n=5e3){return Promise.reject(new Error(`Request not implemented: ${e}`))}on(e,t){let n=String(e);this.eventHandlers.has(n)||this.eventHandlers.set(n,[]),this.eventHandlers.get(n).push(t)}off(e,t){let n=String(e),i=this.eventHandlers.get(n);if(i){let s=i.indexOf(t);s!==-1&&i.splice(s,1)}}removeAllListeners(e){e?this.eventHandlers.delete(String(e)):this.eventHandlers.clear()}handleEvent(e,t){let n=String(e);if(n==="__ch:start"){let{id:s,count:r}=t;this.chunkBuffers.set(s,{count:r,parts:new Array(r),received:0});return}if(n==="__ch:part"){let{id:s,idx:r,chunk:l}=t,d=this.chunkBuffers.get(s);if(d&&(d.parts[r]=l,d.received++,d.received===d.count)){this.chunkBuffers.delete(s);try{let g=d.parts.join(""),[m,C]=this.parsePayload(g);this.handleEvent(m,C)}catch(g){this.error("Failed to reassemble chunked payload",g)}}return}let i=this.eventHandlers.get(n);i&&i.forEach(s=>s(t)),n==="pong"&&(this.latency=Date.now()-this.pingTimestamp,this.log(`Ping: ${this.latency}ms`))}startPingMonitor(){this.stopPingMonitor(),this.pingInterval=setInterval(()=>{this.isConnected()&&(this.pingTimestamp=Date.now(),this.send("ping",null))},2e3)}stopPingMonitor(){this.pingInterval&&(clearInterval(this.pingInterval),this.pingInterval=null)}getPing(){return this.latency}startStatsMonitor(){this.stopStatsMonitor(),this.statsInterval=setInterval(async()=>{if(!(!this.peerConnection||this._state!==c.NetworkState.Connected))try{let e=await this.peerConnection.getStats(),t=null;if(e.forEach(n=>{if(n.type==="transport"){let i=n.selectedCandidatePairId;t=e.get?e.get(i):e[i]}}),t){let n=e,i=n.get?n.get(t.localCandidateId):n[t.localCandidateId],s=n.get?n.get(t.remoteCandidateId):n[t.remoteCandidateId];this.options.debug&&this.log(`RTC Stats: Connection=${t.state} RTT=${t.currentRoundTripTime?Math.round(t.currentRoundTripTime*1e3):"N/A"}ms Path=${i?.candidateType||"?"}<->${s?.candidateType||"?"}`),this.p2pOnly&&(i?.candidateType==="relay"||s?.candidateType==="relay")&&(this.error("STRICT P2P VIOLATION: Detected relay path in stats. DISCONNECTING."),this.disconnect())}}catch(e){this.error("Failed to get RTC stats",e)}},5e3)}stopStatsMonitor(){this.statsInterval&&(clearInterval(this.statsInterval),this.statsInterval=null)}stripRelayFromSDP(e){return e.split(`
|
|
2
|
-
`).filter(
|
|
3
|
-
`)}destroy(){this.disconnect(),this.removeAllListeners()}sendBridge(e
|
|
1
|
+
"use strict";var f=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var T=Object.getOwnPropertyNames;var E=Object.prototype.hasOwnProperty;var P=(h,t,e)=>t in h?f(h,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):h[t]=e;var y=(h,t)=>f(h,"name",{value:t,configurable:!0});var A=(h,t)=>{for(var e in t)f(h,e,{get:t[e],enumerable:!0})},N=(h,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of T(t))!E.call(h,i)&&i!==e&&f(h,i,{get:()=>t[i],enumerable:!(n=R(t,i))||n.enumerable});return h};var $=h=>N(f({},"__esModule",{value:!0}),h);var o=(h,t,e)=>(P(h,typeof t!="symbol"?t+"":t,e),e);var D={};A(D,{NetworkState:()=>I.NetworkState,SocketIOClient:()=>m,WebRTCClient:()=>C});module.exports=$(D);var I=require("@utsp/types");var k=require("socket.io-client"),p=require("@utsp/types");var v=class v{constructor(t){o(this,"socket",null);o(this,"options");o(this,"_state",p.NetworkState.Disconnected);o(this,"pingTimestamp",0);o(this,"latency",0);o(this,"pingInterval",null);o(this,"bridgeHandlers",new Map);o(this,"bridgeSetup",!1);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??{},iceServers:t.iceServers??[],path:t.path,debug:t.debug??!1,sessionId:t.sessionId},this.log("Client initialized",{url:this.options.url,autoReconnect:this.options.autoReconnect})}get state(){return this._state}isConnected(){return this._state===p.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=p.NetworkState.Connecting,this.bridgeSetup=!1,new Promise((t,e)=>{this.socket=(0,k.io)(this.options.url,{path:this.options.path,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=p.NetworkState.Connected,this.startPingMonitor(),this.bridgeHandlers.size>0&&this.setupBridgeListener(),t()}),this.socket.on("connect_error",n=>{this.log(`Connection error: ${n.message}`),this._state=p.NetworkState.Error,e(n)}),this.socket.on("disconnect",n=>{this.log(`Disconnected: ${n}`),this._state=p.NetworkState.Disconnected,this.stopPingMonitor()}),this.socket.on("reconnect_attempt",n=>{this.log(`Reconnection attempt ${n}...`),this._state=p.NetworkState.Reconnecting}),this.socket.on("reconnect",n=>{this.log(`Reconnected after ${n} attempts`),this._state=p.NetworkState.Connected,this.startPingMonitor()}),this.socket.on("reconnect_failed",()=>{this.log("Reconnection failed"),this._state=p.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=p.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.emit(t,e)}catch(n){this.log(`Error sending '${t}':`,n)}}sendVolatile(t,e){if(!(!t||typeof t!="string")&&!(!this.isConnected()||!this.socket))try{this.socket.volatile.emit(t,e)}catch(n){this.log(`Error sending volatile '${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((i,s)=>{let r=`${t}_response`,c=setTimeout(()=>{this.off(r,a),s(new Error(`Request timeout after ${n}ms: ${t}`))},n),a=y(l=>{clearTimeout(c),this.off(r,a),i(l)},"responseHandler");this.on(r,a),this.send(t,e),this.log(`Request sent: ${t} (timeout: ${n}ms)`)})}getPing(){return this.latency}sendBridge(t,e){if(!this.isConnected()||!this.socket){this.log(`Cannot send bridge '${t}': not connected`);return}try{let n={channel:t,data:JSON.stringify(e)};this.socket.emit("bridge",n),this.log(`Bridge sent on channel '${t}'`)}catch(n){this.log(`Error sending bridge: ${n}`)}}onBridge(t,e){this.bridgeHandlers.has(t)||this.bridgeHandlers.set(t,[]),this.bridgeHandlers.get(t).push(e),this.log(`Bridge handler registered for channel '${t}'`),this.setupBridgeListener()}offBridge(t,e){let n=this.bridgeHandlers.get(t);if(n){let i=n.indexOf(e);i!==-1&&(n.splice(i,1),this.log(`Bridge handler removed for channel '${t}'`))}}removeAllBridgeHandlers(t){t?(this.bridgeHandlers.delete(t),this.log(`All bridge handlers removed for channel '${t}'`)):(this.bridgeHandlers.clear(),this.log("All bridge handlers removed"))}setupBridgeListener(){this.bridgeSetup||!this.socket||(this.socket.on("bridge",t=>{this.handleBridgeMessage(t)}),this.bridgeSetup=!0,this.log("Bridge listener setup"))}handleBridgeMessage(t){try{let{channel:e,data:n}=t,i=JSON.parse(n),s=this.bridgeHandlers.get(e);s&&s.length>0?s.forEach(r=>{try{r(i)}catch(c){this.log(`Error in bridge handler for '${e}': ${c}`)}}):this.log(`No bridge handler for channel '${e}'`)}catch(e){this.log(`Error parsing bridge message: ${e}`)}}destroy(){this.log("Destroying client..."),this.stopPingMonitor(),this.bridgeHandlers.clear(),this.bridgeSetup=!1,this.socket&&(this.socket.removeAllListeners(),this.socket.disconnect(),this.socket=null),this._state=p.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}`))}};y(v,"SocketIOClient");var m=v;var w=require("socket.io-client"),d=require("@utsp/types");var b=class b{constructor(t){o(this,"signalingSocket",null);o(this,"peerConnection",null);o(this,"reliableChannel",null);o(this,"unreliableChannel",null);o(this,"options");o(this,"_state",d.NetworkState.Disconnected);o(this,"p2pOnly",!0);o(this,"eventHandlers",new Map);o(this,"bridgeHandlers",new Map);o(this,"chunkBuffers",new Map);o(this,"remotePeerId",null);o(this,"latency",0);o(this,"retryCount",0);o(this,"pingTimestamp",0);o(this,"pingInterval",null);o(this,"statsInterval",null);o(this,"isReassembling",!1);let e=t.sessionId?"https://signal.utsp.dev":void 0,n=t.url||e;if(!n)throw new Error("WebRTCClient: URL required (or sessionId for public relay)");this.options={url:n,autoReconnect:t.autoReconnect??!0,reconnectDelay:t.reconnectDelay??1e3,maxReconnectAttempts:t.maxReconnectAttempts??10,timeout:t.timeout??5e3,auth:t.auth??{},iceServers:t.iceServers??[{urls:"stun:stun.l.google.com:19302"}],debug:t.debug??!1,signalingPath:t.signalingPath??(t.sessionId?"/socket.io":"/utsp-signal"),sessionId:t.sessionId,p2pOnly:t.p2pOnly??!0},this.p2pOnly=this.options.p2pOnly??!0,this.on("bridge",i=>{if(!i||!i.channel)return;let s=i.data;if(typeof s=="string")try{s=JSON.parse(s)}catch{}let r=this.bridgeHandlers.get(i.channel);r&&r.forEach(c=>c(s))})}get state(){return this._state}isConnected(){return this._state===d.NetworkState.Connected}async connect(){if(!(this._state===d.NetworkState.Connected||this._state===d.NetworkState.Connecting))return this._state=d.NetworkState.Connecting,this.log("Connecting via WebRTC..."),new Promise((t,e)=>{this.signalingSocket=(0,w.io)(this.options.url,{path:this.options.signalingPath,auth:this.options.auth,reconnection:this.options.autoReconnect,reconnectionDelay:this.options.reconnectDelay,reconnectionAttempts:this.options.maxReconnectAttempts,transports:["websocket","polling"]}),this.options.sessionId?(this.log(`Connecting via Public Relay to session: ${this.options.sessionId}`),this.signalingSocket.on("connect",()=>{this.log("Connected to Relay"),this.signalingSocket.emit("join-session",this.options.sessionId)}),this.signalingSocket.on("disconnect",i=>{this.warn(`Relay signaled disconnect: ${i}`)}),this.signalingSocket.on("signal",({from:i,type:s,data:r})=>{this.remotePeerId=i,s==="rtc:offer"?(this.log("Received RTC Offer via Relay"),this.handleOffer(r)):s==="rtc:candidate"&&this.peerConnection&&(this.log("Received ICE Candidate via Relay"),this.peerConnection.addIceCandidate(r).catch(c=>this.error("ICE Error",c)))})):(this.signalingSocket.on("connect",()=>{this.log("Signaling connected")}),this.signalingSocket.on("rtc:offer",async i=>{this.log("Received RTC Offer"),await this.handleOffer(i)}),this.signalingSocket.on("rtc:candidate",i=>{let s=this.parseCandidateType(i.candidate);if(this.p2pOnly&&s==="relay"){this.warn("Strict P2P: Dropping received RELAY candidate");return}this.log(`Received ICE Candidate (${s})`),this.peerConnection?.addIceCandidate(new RTCIceCandidate(i))})),this.signalingSocket.on("connect_error",i=>{this.error("Signaling connection error",i),this.signalingSocket&&!this.signalingSocket.active&&(this._state=d.NetworkState.Error,e(i))}),this.signalingSocket.on("disconnect",()=>{this.log("Signaling disconnected"),this._state===d.NetworkState.Connecting&&this.disconnect()}),this.signalingSocket.onAny((i,...s)=>{i.startsWith("rtc:")||i==="signal"||this.handleEvent(i,s[0])});let n=setTimeout(()=>{this._state!==d.NetworkState.Connected&&(this.disconnect(),e(new Error("Connection timeout")))},this.options.timeout);this._connectResolve=()=>{clearTimeout(n),t()}})}async handleOffer(t){this.peerConnection&&this.peerConnection.close(),this.peerConnection=new RTCPeerConnection({iceServers:this.options.iceServers}),this.setupPeerEvents();try{let e=this.p2pOnly?this.stripRelayFromSDP(t.sdp||""):t.sdp;await this.peerConnection.setRemoteDescription(new RTCSessionDescription({type:"offer",sdp:e}));let n=await this.peerConnection.createAnswer(),i=this.p2pOnly?this.stripRelayFromSDP(n.sdp||""):n.sdp||"";await this.peerConnection.setLocalDescription({type:"answer",sdp:i}),this.options.sessionId&&this.remotePeerId?this.signalingSocket?.emit("signal",{target:this.remotePeerId,type:"rtc:answer",data:n}):(this.log("Sending RTC Answer"),this.signalingSocket?.emit("rtc:answer",n))}catch(e){this.error("WebRTC Negotiation failed",e),this.disconnect()}}setupPeerEvents(){this.peerConnection&&(this.peerConnection.onicecandidate=t=>{if(t.candidate)if(this.options.sessionId&&this.remotePeerId)this.signalingSocket?.emit("signal",{target:this.remotePeerId,type:"rtc:candidate",data:t.candidate});else{let e=this.parseCandidateType(t.candidate.candidate);if(this.p2pOnly&&e==="relay"){this.warn("Strict P2P: Not sending RELAY candidate");return}this.log(`Sending ICE Candidate (${e})`),this.signalingSocket?.emit("rtc:candidate",t.candidate)}},this.peerConnection.ondatachannel=t=>{let e=t.channel;this.log(`DataChannel received: ${e.label}`),e.label==="reliable"?(this.reliableChannel=e,this.setupChannel(e),e.onopen=()=>{this.log("Reliable Channel OPEN"),this._state=d.NetworkState.Connected,this.startPingMonitor(),this.startStatsMonitor(),this._connectResolve&&this._connectResolve()}):e.label==="unreliable"&&(this.log("Unreliable Channel RECEIVED"),this.unreliableChannel=e,this.setupChannel(e))},this.peerConnection.onconnectionstatechange=()=>{this.log(`Connection State: ${this.peerConnection?.connectionState}`);let t=this.peerConnection?.connectionState;t==="disconnected"||t==="failed"?this.options.autoReconnect&&this.retryCount<this.options.maxReconnectAttempts?(this.log(`WebRTC lost. Retrying in ${this.options.reconnectDelay}ms... (Attempt ${this.retryCount+1}/${this.options.maxReconnectAttempts})`),this.peerConnection&&(this.peerConnection.close(),this.peerConnection=null),this.retryCount++,setTimeout(()=>{this._state=d.NetworkState.Disconnected,this.connect().catch(e=>this.error("Reconnection failed",e))},this.options.reconnectDelay*Math.pow(1.5,this.retryCount))):this.disconnect():t==="connected"&&(this.retryCount=0)})}parsePayload(t){return JSON.parse(t,(e,n)=>{if(n&&typeof n=="object"&&n._b64){let i=atob(n._b64),s=new Uint8Array(i.length);for(let r=0;r<i.length;r++)s[r]=i.charCodeAt(r);return s}return n})}setupChannel(t){t.binaryType="arraybuffer",t.onmessage=e=>{let n=e.data;if(t.label,n instanceof ArrayBuffer){try{let i=new DataView(n),s=i.getUint8(0);if(s===255){let g=i.getUint8(1),S=new Uint8Array(n,2);this.handleEvent(g,S);return}let r=s,c=new Uint8Array(n,1,r),l=new TextDecoder("utf-8").decode(c),u=new Uint8Array(n,1+r);this.handleEvent(l,u)}catch(i){this.error("Failed to parse binary packet",i)}return}try{let[i,s]=this.parsePayload(n);this.handleEvent(i,s)}catch{}}}disconnect(){this.stopPingMonitor(),this.statsInterval&&clearInterval(this.statsInterval),this.peerConnection&&(this.peerConnection.close(),this.peerConnection=null),this.signalingSocket&&(this.signalingSocket.disconnect(),this.signalingSocket=null),this.reliableChannel=null,this.unreliableChannel=null,this.chunkBuffers.clear(),this._state=d.NetworkState.Disconnected}getChunks(t,e){let n=Math.ceil(t.length/e),i=new Array(n);for(let s=0,r=0;s<n;++s,r+=e)i[s]=t.substr(r,e);return i}preparePayload(t,e){return JSON.stringify([t,e],(n,i)=>{if(i&&typeof i=="object"){if(i instanceof Uint8Array||i instanceof Int8Array||i instanceof Uint16Array||i instanceof Int16Array||i instanceof Uint32Array||i instanceof Int32Array||i instanceof Float32Array||i instanceof Float64Array){let s=new Uint8Array(i.buffer,i.byteOffset,i.byteLength),r="",c=s.byteLength;for(let a=0;a<c;a++)r+=String.fromCharCode(s[a]);return{_b64:btoa(r)}}if(i instanceof ArrayBuffer){let s=new Uint8Array(i),r="",c=s.byteLength;for(let a=0;a<c;a++)r+=String.fromCharCode(s[a]);return{_b64:btoa(r)}}}return i})}send(t,e){this.reliableChannel?.readyState==="open"&&this.transmit(this.reliableChannel,t,e,!0)}sendVolatile(t,e){let n=this.unreliableChannel?.readyState==="open"?this.unreliableChannel:this.reliableChannel;n?.readyState==="open"&&this.transmit(n,t,e,!1)}transmit(t,e,n,i){if(!i&&t.bufferedAmount>64*1024)return;let s=1200;if((n instanceof Uint8Array||n instanceof ArrayBuffer)&&typeof e=="number"&&n.byteLength<=s){let l=new Uint8Array(2);l[0]=255,l[1]=e;let u=n instanceof ArrayBuffer?new Uint8Array(n):n,g=new Uint8Array(l.length+u.length);g.set(l),g.set(u,l.length),t.send(g);return}let c=typeof e=="number"?String(e):e,a=this.preparePayload(c,n);if(a.length<=s)t.send(a);else{let l=Math.random().toString(36).substring(2,15),u=this.getChunks(a,s);t.send(JSON.stringify(["__ch:start",{id:l,count:u.length}]));for(let g=0;g<u.length;g++)t.send(JSON.stringify(["__ch:part",{id:l,idx:g,chunk:u[g]}]))}}request(t,e,n=5e3){return Promise.reject(new Error(`Request not implemented: ${t}`))}on(t,e){let n=String(t);this.eventHandlers.has(n)||this.eventHandlers.set(n,[]),this.eventHandlers.get(n).push(e)}off(t,e){let n=String(t),i=this.eventHandlers.get(n);if(i){let s=i.indexOf(e);s!==-1&&i.splice(s,1)}}removeAllListeners(t){t?this.eventHandlers.delete(String(t)):this.eventHandlers.clear()}handleEvent(t,e){let n=String(t);if(n==="__ch:start"){if(this.isReassembling)return;let{id:s,count:r}=e;this.chunkBuffers.set(s,{count:r,parts:new Array(r),received:0});return}if(n==="__ch:part"){let{id:s,idx:r,chunk:c}=e,a=this.chunkBuffers.get(s);if(a&&(a.parts[r]=c,a.received++,a.received===a.count)){this.chunkBuffers.delete(s);try{this.isReassembling=!0;let l=a.parts.join(""),[u,g]=this.parsePayload(l);this.handleEvent(u,g)}catch(l){this.error("Failed to reassemble chunked payload",l)}finally{this.isReassembling=!1}}return}let i=this.eventHandlers.get(n);i&&i.forEach(s=>s(e)),n==="pong"&&(this.latency=Date.now()-this.pingTimestamp,this.log(`Ping: ${this.latency}ms`))}startPingMonitor(){this.stopPingMonitor(),this.pingInterval=setInterval(()=>{this.isConnected()&&(this.pingTimestamp=Date.now(),this.send("ping",null))},2e3)}stopPingMonitor(){this.pingInterval&&(clearInterval(this.pingInterval),this.pingInterval=null)}getPing(){return this.latency}startStatsMonitor(){this.stopStatsMonitor(),this.statsInterval=setInterval(async()=>{if(!(!this.peerConnection||this._state!==d.NetworkState.Connected))try{let t=await this.peerConnection.getStats(),e=null;if(t.forEach(n=>{if(n.type==="transport"){let i=n.selectedCandidatePairId;e=t.get?t.get(i):t[i]}}),e){let n=t,i=n.get?n.get(e.localCandidateId):n[e.localCandidateId],s=n.get?n.get(e.remoteCandidateId):n[e.remoteCandidateId];this.options.debug&&this.log(`RTC Stats: Connection=${e.state} RTT=${e.currentRoundTripTime?Math.round(e.currentRoundTripTime*1e3):"N/A"}ms Path=${i?.candidateType||"?"}<->${s?.candidateType||"?"}`),this.p2pOnly&&(i?.candidateType==="relay"||s?.candidateType==="relay")&&(this.error("STRICT P2P VIOLATION: Detected relay path in stats. DISCONNECTING."),this.disconnect())}}catch(t){this.error("Failed to get RTC stats",t)}},5e3)}stopStatsMonitor(){this.statsInterval&&(clearInterval(this.statsInterval),this.statsInterval=null)}stripRelayFromSDP(t){return t.split(`
|
|
2
|
+
`).filter(e=>!e.includes("typ relay")).join(`
|
|
3
|
+
`)}destroy(){this.disconnect(),this.removeAllListeners()}sendBridge(t,e){this.send("bridge",{channel:t,data:e})}onBridge(t,e){this.bridgeHandlers.has(t)||this.bridgeHandlers.set(t,[]),this.bridgeHandlers.get(t).push(e)}offBridge(t,e){let n=this.bridgeHandlers.get(t);if(n){let i=n.indexOf(e);i!==-1&&n.splice(i,1)}}removeAllBridgeHandlers(t){t?this.bridgeHandlers.delete(t):this.bridgeHandlers.clear()}log(t,...e){this.options.debug&&console.warn(`[WebRTC-Client] ${t}`,...e)}parseCandidateType(t){return t?t.includes("typ host")?"host":t.includes("typ srflx")?"srflx":t.includes("typ relay")?"relay":"unknown":"unknown"}warn(t,...e){this.options.debug&&console.warn(`[WebRTC-Client] ${t}`,...e)}error(t,...e){console.error(`[WebRTC-Client] \u274C ${t}`,...e)}};y(b,"WebRTCClient");var C=b;0&&(module.exports={NetworkState,SocketIOClient,WebRTCClient});
|
package/dist/index.d.ts
CHANGED
|
@@ -437,13 +437,22 @@ declare class WebRTCClient implements INetworkClient {
|
|
|
437
437
|
private parsePayload;
|
|
438
438
|
private setupChannel;
|
|
439
439
|
disconnect(): void;
|
|
440
|
+
/**
|
|
441
|
+
* Helper to split string into chunks
|
|
442
|
+
*/
|
|
443
|
+
private getChunks;
|
|
440
444
|
private preparePayload;
|
|
441
445
|
send(event: string | NetworkEvent, data: any): void;
|
|
442
446
|
sendVolatile(event: string | NetworkEvent, data: any): void;
|
|
447
|
+
/**
|
|
448
|
+
* Internal transmit with chunking and backpressure
|
|
449
|
+
*/
|
|
450
|
+
private transmit;
|
|
443
451
|
request<T = any>(event: string, data: any, timeout?: number): Promise<T>;
|
|
444
452
|
on<T = any>(event: string | NetworkEvent, handler: NetworkEventHandler<T>): void;
|
|
445
453
|
off<T = any>(event: string | NetworkEvent, handler: NetworkEventHandler<T>): void;
|
|
446
454
|
removeAllListeners(event?: string | NetworkEvent): void;
|
|
455
|
+
private isReassembling;
|
|
447
456
|
private handleEvent;
|
|
448
457
|
/**
|
|
449
458
|
* Start sending pings every 2 seconds
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var k=Object.defineProperty;var w=(d,e,t)=>e in d?k(d,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):d[e]=t;var p=(d,e)=>k(d,"name",{value:e,configurable:!0});var o=(d,e,t)=>(w(d,typeof e!="symbol"?e+"":e,t),t);import{NetworkState as U}from"@utsp/types";import{io as I}from"socket.io-client";import{NetworkState as h}from"@utsp/types";var m=class m{constructor(e){o(this,"socket",null);o(this,"options");o(this,"_state",h.Disconnected);o(this,"pingTimestamp",0);o(this,"latency",0);o(this,"pingInterval",null);o(this,"bridgeHandlers",new Map);o(this,"bridgeSetup",!1);if(!e.url||typeof e.url!="string")throw new Error("SocketIOClient: Invalid URL - must be a non-empty string");try{if(!new globalThis.URL(e.url).protocol)throw new Error("Invalid protocol")}catch{throw new Error(`SocketIOClient: Invalid URL format - ${e.url}`)}if(e.reconnectDelay!==void 0&&(e.reconnectDelay<0||!Number.isFinite(e.reconnectDelay)))throw new Error(`SocketIOClient: Invalid reconnectDelay - must be a positive number (got ${e.reconnectDelay})`);if(e.maxReconnectAttempts!==void 0&&(!Number.isInteger(e.maxReconnectAttempts)||e.maxReconnectAttempts<0))throw new Error(`SocketIOClient: Invalid maxReconnectAttempts - must be a positive integer (got ${e.maxReconnectAttempts})`);if(e.timeout!==void 0&&(e.timeout<=0||!Number.isFinite(e.timeout)))throw new Error(`SocketIOClient: Invalid timeout - must be a positive number (got ${e.timeout})`);this.options={url:e.url,autoReconnect:e.autoReconnect??!0,reconnectDelay:e.reconnectDelay??1e3,maxReconnectAttempts:e.maxReconnectAttempts??10,timeout:e.timeout??5e3,auth:e.auth??{},iceServers:e.iceServers??[],path:e.path,debug:e.debug??!1,sessionId:e.sessionId},this.log("Client initialized",{url:this.options.url,autoReconnect:this.options.autoReconnect})}get state(){return this._state}isConnected(){return this._state===h.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=h.Connecting,this.bridgeSetup=!1,new Promise((e,t)=>{this.socket=I(this.options.url,{path:this.options.path,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=h.Connected,this.startPingMonitor(),this.bridgeHandlers.size>0&&this.setupBridgeListener(),e()}),this.socket.on("connect_error",n=>{this.log(`Connection error: ${n.message}`),this._state=h.Error,t(n)}),this.socket.on("disconnect",n=>{this.log(`Disconnected: ${n}`),this._state=h.Disconnected,this.stopPingMonitor()}),this.socket.on("reconnect_attempt",n=>{this.log(`Reconnection attempt ${n}...`),this._state=h.Reconnecting}),this.socket.on("reconnect",n=>{this.log(`Reconnected after ${n} attempts`),this._state=h.Connected,this.startPingMonitor()}),this.socket.on("reconnect_failed",()=>{this.log("Reconnection failed"),this._state=h.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=h.Disconnected)}send(e,t){if(!e||typeof e!="string"){this.log(`Cannot send: invalid event name (${typeof e})`);return}if(!this.isConnected()||!this.socket){this.log(`Cannot send '${e}': not connected`);return}try{this.socket.emit(e,t)}catch(n){this.log(`Error sending '${e}':`,n)}}sendVolatile(e,t){if(!(!e||typeof e!="string")&&!(!this.isConnected()||!this.socket))try{this.socket.volatile.emit(e,t)}catch(n){this.log(`Error sending volatile '${e}':`,n)}}on(e,t){if(!e||typeof e!="string")throw new Error(`Invalid event name: ${e}`);if(typeof t!="function")throw new Error(`Invalid handler for event '${e}': must be a function`);if(!this.socket){this.log(`Cannot listen to '${e}': socket not initialized`);return}this.socket.on(e,t),this.log(`Listening to '${e}'`)}off(e,t){this.socket&&(this.socket.off(e,t),this.log(`Stopped listening to '${e}'`))}removeAllListeners(e){this.socket&&(e?(this.socket.removeAllListeners(e),this.log(`Removed all listeners for '${e}'`)):(this.socket.removeAllListeners(),this.log("Removed all listeners")))}async request(e,t,n=5e3){if(!e||typeof e!="string")throw new Error(`Invalid event name: ${e}`);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((i,s)=>{let r=`${e}_response`,a=setTimeout(()=>{this.off(r,c),s(new Error(`Request timeout after ${n}ms: ${e}`))},n),c=p(g=>{clearTimeout(a),this.off(r,c),i(g)},"responseHandler");this.on(r,c),this.send(e,t),this.log(`Request sent: ${e} (timeout: ${n}ms)`)})}getPing(){return this.latency}sendBridge(e,t){if(!this.isConnected()||!this.socket){this.log(`Cannot send bridge '${e}': not connected`);return}try{let n={channel:e,data:JSON.stringify(t)};this.socket.emit("bridge",n),this.log(`Bridge sent on channel '${e}'`)}catch(n){this.log(`Error sending bridge: ${n}`)}}onBridge(e,t){this.bridgeHandlers.has(e)||this.bridgeHandlers.set(e,[]),this.bridgeHandlers.get(e).push(t),this.log(`Bridge handler registered for channel '${e}'`),this.setupBridgeListener()}offBridge(e,t){let n=this.bridgeHandlers.get(e);if(n){let i=n.indexOf(t);i!==-1&&(n.splice(i,1),this.log(`Bridge handler removed for channel '${e}'`))}}removeAllBridgeHandlers(e){e?(this.bridgeHandlers.delete(e),this.log(`All bridge handlers removed for channel '${e}'`)):(this.bridgeHandlers.clear(),this.log("All bridge handlers removed"))}setupBridgeListener(){this.bridgeSetup||!this.socket||(this.socket.on("bridge",e=>{this.handleBridgeMessage(e)}),this.bridgeSetup=!0,this.log("Bridge listener setup"))}handleBridgeMessage(e){try{let{channel:t,data:n}=e,i=JSON.parse(n),s=this.bridgeHandlers.get(t);s&&s.length>0?s.forEach(r=>{try{r(i)}catch(a){this.log(`Error in bridge handler for '${t}': ${a}`)}}):this.log(`No bridge handler for channel '${t}'`)}catch(t){this.log(`Error parsing bridge message: ${t}`)}}destroy(){this.log("Destroying client..."),this.stopPingMonitor(),this.bridgeHandlers.clear(),this.bridgeSetup=!1,this.socket&&(this.socket.removeAllListeners(),this.socket.disconnect(),this.socket=null),this._state=h.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(e,t){this.options.debug&&(t!==void 0?console.warn(`[SocketIOClient] ${e}`,t):console.warn(`[SocketIOClient] ${e}`))}};p(m,"SocketIOClient");var y=m;import{io as S}from"socket.io-client";import{NetworkState as l}from"@utsp/types";var v=class v{constructor(e){o(this,"signalingSocket",null);o(this,"peerConnection",null);o(this,"reliableChannel",null);o(this,"unreliableChannel",null);o(this,"options");o(this,"_state",l.Disconnected);o(this,"p2pOnly",!0);o(this,"eventHandlers",new Map);o(this,"bridgeHandlers",new Map);o(this,"chunkBuffers",new Map);o(this,"remotePeerId",null);o(this,"latency",0);o(this,"retryCount",0);o(this,"pingTimestamp",0);o(this,"pingInterval",null);o(this,"statsInterval",null);let t=e.sessionId?"https://signal.utsp.dev":void 0,n=e.url||t;if(!n)throw new Error("WebRTCClient: URL required (or sessionId for public relay)");this.options={url:n,autoReconnect:e.autoReconnect??!0,reconnectDelay:e.reconnectDelay??1e3,maxReconnectAttempts:e.maxReconnectAttempts??10,timeout:e.timeout??5e3,auth:e.auth??{},iceServers:e.iceServers??[{urls:"stun:stun.l.google.com:19302"}],debug:e.debug??!1,signalingPath:e.signalingPath??(e.sessionId?"/socket.io":"/utsp-signal"),sessionId:e.sessionId,p2pOnly:e.p2pOnly??!0},this.p2pOnly=this.options.p2pOnly??!0,this.on("bridge",i=>{if(!i||!i.channel)return;let s=i.data;if(typeof s=="string")try{s=JSON.parse(s)}catch{}let r=this.bridgeHandlers.get(i.channel);r&&r.forEach(a=>a(s))})}get state(){return this._state}isConnected(){return this._state===l.Connected}async connect(){if(!(this._state===l.Connected||this._state===l.Connecting))return this._state=l.Connecting,this.log("Connecting via WebRTC..."),new Promise((e,t)=>{this.signalingSocket=S(this.options.url,{path:this.options.signalingPath,auth:this.options.auth,reconnection:this.options.autoReconnect,reconnectionDelay:this.options.reconnectDelay,reconnectionAttempts:this.options.maxReconnectAttempts,transports:["websocket","polling"]}),this.options.sessionId?(this.log(`Connecting via Public Relay to session: ${this.options.sessionId}`),this.signalingSocket.on("connect",()=>{this.log("Connected to Relay"),this.signalingSocket.emit("join-session",this.options.sessionId)}),this.signalingSocket.on("disconnect",i=>{this.warn(`Relay signaled disconnect: ${i}`)}),this.signalingSocket.on("signal",({from:i,type:s,data:r})=>{this.remotePeerId=i,s==="rtc:offer"?(this.log("Received RTC Offer via Relay"),this.handleOffer(r)):s==="rtc:candidate"&&this.peerConnection&&(this.log("Received ICE Candidate via Relay"),this.peerConnection.addIceCandidate(r).catch(a=>this.error("ICE Error",a)))})):(this.signalingSocket.on("connect",()=>{this.log("Signaling connected")}),this.signalingSocket.on("rtc:offer",async i=>{this.log("Received RTC Offer"),await this.handleOffer(i)}),this.signalingSocket.on("rtc:candidate",i=>{let s=this.parseCandidateType(i.candidate);if(this.p2pOnly&&s==="relay"){this.warn("Strict P2P: Dropping received RELAY candidate");return}this.log(`Received ICE Candidate (${s})`),this.peerConnection?.addIceCandidate(new RTCIceCandidate(i))})),this.signalingSocket.on("connect_error",i=>{this.error("Signaling connection error",i),this.signalingSocket&&!this.signalingSocket.active&&(this._state=l.Error,t(i))}),this.signalingSocket.on("disconnect",()=>{this.log("Signaling disconnected"),this._state===l.Connecting&&this.disconnect()}),this.signalingSocket.onAny((i,...s)=>{i.startsWith("rtc:")||i==="signal"||this.handleEvent(i,s[0])});let n=setTimeout(()=>{this._state!==l.Connected&&(this.disconnect(),t(new Error("Connection timeout")))},this.options.timeout);this._connectResolve=()=>{clearTimeout(n),e()}})}async handleOffer(e){this.peerConnection&&this.peerConnection.close(),this.peerConnection=new RTCPeerConnection({iceServers:this.options.iceServers}),this.setupPeerEvents();try{let t=this.p2pOnly?this.stripRelayFromSDP(e.sdp||""):e.sdp;await this.peerConnection.setRemoteDescription(new RTCSessionDescription({type:"offer",sdp:t}));let n=await this.peerConnection.createAnswer(),i=this.p2pOnly?this.stripRelayFromSDP(n.sdp||""):n.sdp||"";await this.peerConnection.setLocalDescription({type:"answer",sdp:i}),this.options.sessionId&&this.remotePeerId?this.signalingSocket?.emit("signal",{target:this.remotePeerId,type:"rtc:answer",data:n}):(this.log("Sending RTC Answer"),this.signalingSocket?.emit("rtc:answer",n))}catch(t){this.error("WebRTC Negotiation failed",t),this.disconnect()}}setupPeerEvents(){this.peerConnection&&(this.peerConnection.onicecandidate=e=>{if(e.candidate)if(this.options.sessionId&&this.remotePeerId)this.signalingSocket?.emit("signal",{target:this.remotePeerId,type:"rtc:candidate",data:e.candidate});else{let t=this.parseCandidateType(e.candidate.candidate);if(this.p2pOnly&&t==="relay"){this.warn("Strict P2P: Not sending RELAY candidate");return}this.log(`Sending ICE Candidate (${t})`),this.signalingSocket?.emit("rtc:candidate",e.candidate)}},this.peerConnection.ondatachannel=e=>{let t=e.channel;this.log(`DataChannel received: ${t.label}`),t.label==="reliable"?(this.reliableChannel=t,this.setupChannel(t),t.onopen=()=>{this.log("Reliable Channel OPEN"),this._state=l.Connected,this.startPingMonitor(),this.startStatsMonitor(),this._connectResolve&&this._connectResolve()}):t.label==="unreliable"&&(this.log("Unreliable Channel RECEIVED"),this.unreliableChannel=t,this.setupChannel(t))},this.peerConnection.onconnectionstatechange=()=>{this.log(`Connection State: ${this.peerConnection?.connectionState}`);let e=this.peerConnection?.connectionState;e==="disconnected"||e==="failed"?this.options.autoReconnect&&this.retryCount<this.options.maxReconnectAttempts?(this.log(`WebRTC lost. Retrying in ${this.options.reconnectDelay}ms... (Attempt ${this.retryCount+1}/${this.options.maxReconnectAttempts})`),this.peerConnection&&(this.peerConnection.close(),this.peerConnection=null),this.retryCount++,setTimeout(()=>{this._state=l.Disconnected,this.connect().catch(t=>this.error("Reconnection failed",t))},this.options.reconnectDelay*Math.pow(1.5,this.retryCount))):this.disconnect():e==="connected"&&(this.retryCount=0)})}parsePayload(e){return JSON.parse(e,(t,n)=>{if(n&&typeof n=="object"&&n._b64){let i=atob(n._b64),s=new Uint8Array(i.length);for(let r=0;r<i.length;r++)s[r]=i.charCodeAt(r);return s}return n})}setupChannel(e){e.binaryType="arraybuffer",e.onmessage=t=>{let n=t.data;if(e.label,n instanceof ArrayBuffer){try{let i=new DataView(n),s=i.getUint8(0);if(s===255){let f=i.getUint8(1),b=new Uint8Array(n,2);this.handleEvent(f,b);return}let r=s,a=new Uint8Array(n,1,r),g=new TextDecoder("utf-8").decode(a),u=new Uint8Array(n,1+r);this.handleEvent(g,u)}catch(i){this.error("Failed to parse binary packet",i)}return}try{let[i,s]=this.parsePayload(n);this.handleEvent(i,s)}catch{}}}disconnect(){this._state=l.Disconnected,this.reliableChannel&&this.reliableChannel.close(),this.unreliableChannel&&this.unreliableChannel.close(),this.peerConnection&&this.peerConnection.close(),this.signalingSocket&&this.signalingSocket.disconnect(),this.stopPingMonitor(),this.stopStatsMonitor(),this.reliableChannel=null,this.unreliableChannel=null,this.peerConnection=null,this.signalingSocket=null}preparePayload(e,t){let n=t;return t instanceof Uint8Array||t instanceof Int8Array||t instanceof Uint16Array||t instanceof Int16Array||t instanceof Uint32Array||t instanceof Int32Array||t instanceof Float32Array||t instanceof Float64Array?n=Array.from(t):t instanceof ArrayBuffer&&(n=Array.from(new Uint8Array(t))),JSON.stringify([e,n])}send(e,t){if(this.reliableChannel?.readyState==="open"){if(typeof e=="number"&&(t instanceof Uint8Array||t instanceof ArrayBuffer)){let i=new Uint8Array(2);i[0]=255,i[1]=e;let s=t instanceof ArrayBuffer?new Uint8Array(t):t,r=new Uint8Array(i.length+s.length);r.set(i),r.set(s,i.length),this.reliableChannel.send(r);return}if(typeof e=="number"){let i=this.preparePayload(String(e),t);this.reliableChannel.send(i);return}let n=this.preparePayload(e,t);this.reliableChannel.send(n)}}sendVolatile(e,t){let n=this.unreliableChannel?.readyState==="open"?this.unreliableChannel:this.reliableChannel;if(n?.readyState==="open"){if(n.bufferedAmount>16*1024){this.warn(`Backpressure: Dropping volatile packet on ${n.label} (buffered: ${n.bufferedAmount} bytes)`);return}if(typeof e=="number"&&(t instanceof Uint8Array||t instanceof ArrayBuffer)){let s=new Uint8Array(2);s[0]=255,s[1]=e;let r=t instanceof ArrayBuffer?new Uint8Array(t):t,a=new Uint8Array(s.length+r.length);a.set(s),a.set(r,s.length),n.send(a);return}let i=this.preparePayload(String(e),t);n.send(i)}}request(e,t,n=5e3){return Promise.reject(new Error(`Request not implemented: ${e}`))}on(e,t){let n=String(e);this.eventHandlers.has(n)||this.eventHandlers.set(n,[]),this.eventHandlers.get(n).push(t)}off(e,t){let n=String(e),i=this.eventHandlers.get(n);if(i){let s=i.indexOf(t);s!==-1&&i.splice(s,1)}}removeAllListeners(e){e?this.eventHandlers.delete(String(e)):this.eventHandlers.clear()}handleEvent(e,t){let n=String(e);if(n==="__ch:start"){let{id:s,count:r}=t;this.chunkBuffers.set(s,{count:r,parts:new Array(r),received:0});return}if(n==="__ch:part"){let{id:s,idx:r,chunk:a}=t,c=this.chunkBuffers.get(s);if(c&&(c.parts[r]=a,c.received++,c.received===c.count)){this.chunkBuffers.delete(s);try{let g=c.parts.join(""),[u,f]=this.parsePayload(g);this.handleEvent(u,f)}catch(g){this.error("Failed to reassemble chunked payload",g)}}return}let i=this.eventHandlers.get(n);i&&i.forEach(s=>s(t)),n==="pong"&&(this.latency=Date.now()-this.pingTimestamp,this.log(`Ping: ${this.latency}ms`))}startPingMonitor(){this.stopPingMonitor(),this.pingInterval=setInterval(()=>{this.isConnected()&&(this.pingTimestamp=Date.now(),this.send("ping",null))},2e3)}stopPingMonitor(){this.pingInterval&&(clearInterval(this.pingInterval),this.pingInterval=null)}getPing(){return this.latency}startStatsMonitor(){this.stopStatsMonitor(),this.statsInterval=setInterval(async()=>{if(!(!this.peerConnection||this._state!==l.Connected))try{let e=await this.peerConnection.getStats(),t=null;if(e.forEach(n=>{if(n.type==="transport"){let i=n.selectedCandidatePairId;t=e.get?e.get(i):e[i]}}),t){let n=e,i=n.get?n.get(t.localCandidateId):n[t.localCandidateId],s=n.get?n.get(t.remoteCandidateId):n[t.remoteCandidateId];this.options.debug&&this.log(`RTC Stats: Connection=${t.state} RTT=${t.currentRoundTripTime?Math.round(t.currentRoundTripTime*1e3):"N/A"}ms Path=${i?.candidateType||"?"}<->${s?.candidateType||"?"}`),this.p2pOnly&&(i?.candidateType==="relay"||s?.candidateType==="relay")&&(this.error("STRICT P2P VIOLATION: Detected relay path in stats. DISCONNECTING."),this.disconnect())}}catch(e){this.error("Failed to get RTC stats",e)}},5e3)}stopStatsMonitor(){this.statsInterval&&(clearInterval(this.statsInterval),this.statsInterval=null)}stripRelayFromSDP(e){return e.split(`
|
|
2
|
-
`).filter(
|
|
3
|
-
`)}destroy(){this.disconnect(),this.removeAllListeners()}sendBridge(e
|
|
1
|
+
var b=Object.defineProperty;var w=(u,t,e)=>t in u?b(u,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):u[t]=e;var f=(u,t)=>b(u,"name",{value:t,configurable:!0});var o=(u,t,e)=>(w(u,typeof t!="symbol"?t+"":t,e),e);import{NetworkState as U}from"@utsp/types";import{io as I}from"socket.io-client";import{NetworkState as g}from"@utsp/types";var m=class m{constructor(t){o(this,"socket",null);o(this,"options");o(this,"_state",g.Disconnected);o(this,"pingTimestamp",0);o(this,"latency",0);o(this,"pingInterval",null);o(this,"bridgeHandlers",new Map);o(this,"bridgeSetup",!1);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??{},iceServers:t.iceServers??[],path:t.path,debug:t.debug??!1,sessionId:t.sessionId},this.log("Client initialized",{url:this.options.url,autoReconnect:this.options.autoReconnect})}get state(){return this._state}isConnected(){return this._state===g.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=g.Connecting,this.bridgeSetup=!1,new Promise((t,e)=>{this.socket=I(this.options.url,{path:this.options.path,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=g.Connected,this.startPingMonitor(),this.bridgeHandlers.size>0&&this.setupBridgeListener(),t()}),this.socket.on("connect_error",n=>{this.log(`Connection error: ${n.message}`),this._state=g.Error,e(n)}),this.socket.on("disconnect",n=>{this.log(`Disconnected: ${n}`),this._state=g.Disconnected,this.stopPingMonitor()}),this.socket.on("reconnect_attempt",n=>{this.log(`Reconnection attempt ${n}...`),this._state=g.Reconnecting}),this.socket.on("reconnect",n=>{this.log(`Reconnected after ${n} attempts`),this._state=g.Connected,this.startPingMonitor()}),this.socket.on("reconnect_failed",()=>{this.log("Reconnection failed"),this._state=g.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=g.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.emit(t,e)}catch(n){this.log(`Error sending '${t}':`,n)}}sendVolatile(t,e){if(!(!t||typeof t!="string")&&!(!this.isConnected()||!this.socket))try{this.socket.volatile.emit(t,e)}catch(n){this.log(`Error sending volatile '${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((i,s)=>{let r=`${t}_response`,c=setTimeout(()=>{this.off(r,a),s(new Error(`Request timeout after ${n}ms: ${t}`))},n),a=f(l=>{clearTimeout(c),this.off(r,a),i(l)},"responseHandler");this.on(r,a),this.send(t,e),this.log(`Request sent: ${t} (timeout: ${n}ms)`)})}getPing(){return this.latency}sendBridge(t,e){if(!this.isConnected()||!this.socket){this.log(`Cannot send bridge '${t}': not connected`);return}try{let n={channel:t,data:JSON.stringify(e)};this.socket.emit("bridge",n),this.log(`Bridge sent on channel '${t}'`)}catch(n){this.log(`Error sending bridge: ${n}`)}}onBridge(t,e){this.bridgeHandlers.has(t)||this.bridgeHandlers.set(t,[]),this.bridgeHandlers.get(t).push(e),this.log(`Bridge handler registered for channel '${t}'`),this.setupBridgeListener()}offBridge(t,e){let n=this.bridgeHandlers.get(t);if(n){let i=n.indexOf(e);i!==-1&&(n.splice(i,1),this.log(`Bridge handler removed for channel '${t}'`))}}removeAllBridgeHandlers(t){t?(this.bridgeHandlers.delete(t),this.log(`All bridge handlers removed for channel '${t}'`)):(this.bridgeHandlers.clear(),this.log("All bridge handlers removed"))}setupBridgeListener(){this.bridgeSetup||!this.socket||(this.socket.on("bridge",t=>{this.handleBridgeMessage(t)}),this.bridgeSetup=!0,this.log("Bridge listener setup"))}handleBridgeMessage(t){try{let{channel:e,data:n}=t,i=JSON.parse(n),s=this.bridgeHandlers.get(e);s&&s.length>0?s.forEach(r=>{try{r(i)}catch(c){this.log(`Error in bridge handler for '${e}': ${c}`)}}):this.log(`No bridge handler for channel '${e}'`)}catch(e){this.log(`Error parsing bridge message: ${e}`)}}destroy(){this.log("Destroying client..."),this.stopPingMonitor(),this.bridgeHandlers.clear(),this.bridgeSetup=!1,this.socket&&(this.socket.removeAllListeners(),this.socket.disconnect(),this.socket=null),this._state=g.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}`))}};f(m,"SocketIOClient");var y=m;import{io as S}from"socket.io-client";import{NetworkState as d}from"@utsp/types";var v=class v{constructor(t){o(this,"signalingSocket",null);o(this,"peerConnection",null);o(this,"reliableChannel",null);o(this,"unreliableChannel",null);o(this,"options");o(this,"_state",d.Disconnected);o(this,"p2pOnly",!0);o(this,"eventHandlers",new Map);o(this,"bridgeHandlers",new Map);o(this,"chunkBuffers",new Map);o(this,"remotePeerId",null);o(this,"latency",0);o(this,"retryCount",0);o(this,"pingTimestamp",0);o(this,"pingInterval",null);o(this,"statsInterval",null);o(this,"isReassembling",!1);let e=t.sessionId?"https://signal.utsp.dev":void 0,n=t.url||e;if(!n)throw new Error("WebRTCClient: URL required (or sessionId for public relay)");this.options={url:n,autoReconnect:t.autoReconnect??!0,reconnectDelay:t.reconnectDelay??1e3,maxReconnectAttempts:t.maxReconnectAttempts??10,timeout:t.timeout??5e3,auth:t.auth??{},iceServers:t.iceServers??[{urls:"stun:stun.l.google.com:19302"}],debug:t.debug??!1,signalingPath:t.signalingPath??(t.sessionId?"/socket.io":"/utsp-signal"),sessionId:t.sessionId,p2pOnly:t.p2pOnly??!0},this.p2pOnly=this.options.p2pOnly??!0,this.on("bridge",i=>{if(!i||!i.channel)return;let s=i.data;if(typeof s=="string")try{s=JSON.parse(s)}catch{}let r=this.bridgeHandlers.get(i.channel);r&&r.forEach(c=>c(s))})}get state(){return this._state}isConnected(){return this._state===d.Connected}async connect(){if(!(this._state===d.Connected||this._state===d.Connecting))return this._state=d.Connecting,this.log("Connecting via WebRTC..."),new Promise((t,e)=>{this.signalingSocket=S(this.options.url,{path:this.options.signalingPath,auth:this.options.auth,reconnection:this.options.autoReconnect,reconnectionDelay:this.options.reconnectDelay,reconnectionAttempts:this.options.maxReconnectAttempts,transports:["websocket","polling"]}),this.options.sessionId?(this.log(`Connecting via Public Relay to session: ${this.options.sessionId}`),this.signalingSocket.on("connect",()=>{this.log("Connected to Relay"),this.signalingSocket.emit("join-session",this.options.sessionId)}),this.signalingSocket.on("disconnect",i=>{this.warn(`Relay signaled disconnect: ${i}`)}),this.signalingSocket.on("signal",({from:i,type:s,data:r})=>{this.remotePeerId=i,s==="rtc:offer"?(this.log("Received RTC Offer via Relay"),this.handleOffer(r)):s==="rtc:candidate"&&this.peerConnection&&(this.log("Received ICE Candidate via Relay"),this.peerConnection.addIceCandidate(r).catch(c=>this.error("ICE Error",c)))})):(this.signalingSocket.on("connect",()=>{this.log("Signaling connected")}),this.signalingSocket.on("rtc:offer",async i=>{this.log("Received RTC Offer"),await this.handleOffer(i)}),this.signalingSocket.on("rtc:candidate",i=>{let s=this.parseCandidateType(i.candidate);if(this.p2pOnly&&s==="relay"){this.warn("Strict P2P: Dropping received RELAY candidate");return}this.log(`Received ICE Candidate (${s})`),this.peerConnection?.addIceCandidate(new RTCIceCandidate(i))})),this.signalingSocket.on("connect_error",i=>{this.error("Signaling connection error",i),this.signalingSocket&&!this.signalingSocket.active&&(this._state=d.Error,e(i))}),this.signalingSocket.on("disconnect",()=>{this.log("Signaling disconnected"),this._state===d.Connecting&&this.disconnect()}),this.signalingSocket.onAny((i,...s)=>{i.startsWith("rtc:")||i==="signal"||this.handleEvent(i,s[0])});let n=setTimeout(()=>{this._state!==d.Connected&&(this.disconnect(),e(new Error("Connection timeout")))},this.options.timeout);this._connectResolve=()=>{clearTimeout(n),t()}})}async handleOffer(t){this.peerConnection&&this.peerConnection.close(),this.peerConnection=new RTCPeerConnection({iceServers:this.options.iceServers}),this.setupPeerEvents();try{let e=this.p2pOnly?this.stripRelayFromSDP(t.sdp||""):t.sdp;await this.peerConnection.setRemoteDescription(new RTCSessionDescription({type:"offer",sdp:e}));let n=await this.peerConnection.createAnswer(),i=this.p2pOnly?this.stripRelayFromSDP(n.sdp||""):n.sdp||"";await this.peerConnection.setLocalDescription({type:"answer",sdp:i}),this.options.sessionId&&this.remotePeerId?this.signalingSocket?.emit("signal",{target:this.remotePeerId,type:"rtc:answer",data:n}):(this.log("Sending RTC Answer"),this.signalingSocket?.emit("rtc:answer",n))}catch(e){this.error("WebRTC Negotiation failed",e),this.disconnect()}}setupPeerEvents(){this.peerConnection&&(this.peerConnection.onicecandidate=t=>{if(t.candidate)if(this.options.sessionId&&this.remotePeerId)this.signalingSocket?.emit("signal",{target:this.remotePeerId,type:"rtc:candidate",data:t.candidate});else{let e=this.parseCandidateType(t.candidate.candidate);if(this.p2pOnly&&e==="relay"){this.warn("Strict P2P: Not sending RELAY candidate");return}this.log(`Sending ICE Candidate (${e})`),this.signalingSocket?.emit("rtc:candidate",t.candidate)}},this.peerConnection.ondatachannel=t=>{let e=t.channel;this.log(`DataChannel received: ${e.label}`),e.label==="reliable"?(this.reliableChannel=e,this.setupChannel(e),e.onopen=()=>{this.log("Reliable Channel OPEN"),this._state=d.Connected,this.startPingMonitor(),this.startStatsMonitor(),this._connectResolve&&this._connectResolve()}):e.label==="unreliable"&&(this.log("Unreliable Channel RECEIVED"),this.unreliableChannel=e,this.setupChannel(e))},this.peerConnection.onconnectionstatechange=()=>{this.log(`Connection State: ${this.peerConnection?.connectionState}`);let t=this.peerConnection?.connectionState;t==="disconnected"||t==="failed"?this.options.autoReconnect&&this.retryCount<this.options.maxReconnectAttempts?(this.log(`WebRTC lost. Retrying in ${this.options.reconnectDelay}ms... (Attempt ${this.retryCount+1}/${this.options.maxReconnectAttempts})`),this.peerConnection&&(this.peerConnection.close(),this.peerConnection=null),this.retryCount++,setTimeout(()=>{this._state=d.Disconnected,this.connect().catch(e=>this.error("Reconnection failed",e))},this.options.reconnectDelay*Math.pow(1.5,this.retryCount))):this.disconnect():t==="connected"&&(this.retryCount=0)})}parsePayload(t){return JSON.parse(t,(e,n)=>{if(n&&typeof n=="object"&&n._b64){let i=atob(n._b64),s=new Uint8Array(i.length);for(let r=0;r<i.length;r++)s[r]=i.charCodeAt(r);return s}return n})}setupChannel(t){t.binaryType="arraybuffer",t.onmessage=e=>{let n=e.data;if(t.label,n instanceof ArrayBuffer){try{let i=new DataView(n),s=i.getUint8(0);if(s===255){let h=i.getUint8(1),k=new Uint8Array(n,2);this.handleEvent(h,k);return}let r=s,c=new Uint8Array(n,1,r),l=new TextDecoder("utf-8").decode(c),p=new Uint8Array(n,1+r);this.handleEvent(l,p)}catch(i){this.error("Failed to parse binary packet",i)}return}try{let[i,s]=this.parsePayload(n);this.handleEvent(i,s)}catch{}}}disconnect(){this.stopPingMonitor(),this.statsInterval&&clearInterval(this.statsInterval),this.peerConnection&&(this.peerConnection.close(),this.peerConnection=null),this.signalingSocket&&(this.signalingSocket.disconnect(),this.signalingSocket=null),this.reliableChannel=null,this.unreliableChannel=null,this.chunkBuffers.clear(),this._state=d.Disconnected}getChunks(t,e){let n=Math.ceil(t.length/e),i=new Array(n);for(let s=0,r=0;s<n;++s,r+=e)i[s]=t.substr(r,e);return i}preparePayload(t,e){return JSON.stringify([t,e],(n,i)=>{if(i&&typeof i=="object"){if(i instanceof Uint8Array||i instanceof Int8Array||i instanceof Uint16Array||i instanceof Int16Array||i instanceof Uint32Array||i instanceof Int32Array||i instanceof Float32Array||i instanceof Float64Array){let s=new Uint8Array(i.buffer,i.byteOffset,i.byteLength),r="",c=s.byteLength;for(let a=0;a<c;a++)r+=String.fromCharCode(s[a]);return{_b64:btoa(r)}}if(i instanceof ArrayBuffer){let s=new Uint8Array(i),r="",c=s.byteLength;for(let a=0;a<c;a++)r+=String.fromCharCode(s[a]);return{_b64:btoa(r)}}}return i})}send(t,e){this.reliableChannel?.readyState==="open"&&this.transmit(this.reliableChannel,t,e,!0)}sendVolatile(t,e){let n=this.unreliableChannel?.readyState==="open"?this.unreliableChannel:this.reliableChannel;n?.readyState==="open"&&this.transmit(n,t,e,!1)}transmit(t,e,n,i){if(!i&&t.bufferedAmount>64*1024)return;let s=1200;if((n instanceof Uint8Array||n instanceof ArrayBuffer)&&typeof e=="number"&&n.byteLength<=s){let l=new Uint8Array(2);l[0]=255,l[1]=e;let p=n instanceof ArrayBuffer?new Uint8Array(n):n,h=new Uint8Array(l.length+p.length);h.set(l),h.set(p,l.length),t.send(h);return}let c=typeof e=="number"?String(e):e,a=this.preparePayload(c,n);if(a.length<=s)t.send(a);else{let l=Math.random().toString(36).substring(2,15),p=this.getChunks(a,s);t.send(JSON.stringify(["__ch:start",{id:l,count:p.length}]));for(let h=0;h<p.length;h++)t.send(JSON.stringify(["__ch:part",{id:l,idx:h,chunk:p[h]}]))}}request(t,e,n=5e3){return Promise.reject(new Error(`Request not implemented: ${t}`))}on(t,e){let n=String(t);this.eventHandlers.has(n)||this.eventHandlers.set(n,[]),this.eventHandlers.get(n).push(e)}off(t,e){let n=String(t),i=this.eventHandlers.get(n);if(i){let s=i.indexOf(e);s!==-1&&i.splice(s,1)}}removeAllListeners(t){t?this.eventHandlers.delete(String(t)):this.eventHandlers.clear()}handleEvent(t,e){let n=String(t);if(n==="__ch:start"){if(this.isReassembling)return;let{id:s,count:r}=e;this.chunkBuffers.set(s,{count:r,parts:new Array(r),received:0});return}if(n==="__ch:part"){let{id:s,idx:r,chunk:c}=e,a=this.chunkBuffers.get(s);if(a&&(a.parts[r]=c,a.received++,a.received===a.count)){this.chunkBuffers.delete(s);try{this.isReassembling=!0;let l=a.parts.join(""),[p,h]=this.parsePayload(l);this.handleEvent(p,h)}catch(l){this.error("Failed to reassemble chunked payload",l)}finally{this.isReassembling=!1}}return}let i=this.eventHandlers.get(n);i&&i.forEach(s=>s(e)),n==="pong"&&(this.latency=Date.now()-this.pingTimestamp,this.log(`Ping: ${this.latency}ms`))}startPingMonitor(){this.stopPingMonitor(),this.pingInterval=setInterval(()=>{this.isConnected()&&(this.pingTimestamp=Date.now(),this.send("ping",null))},2e3)}stopPingMonitor(){this.pingInterval&&(clearInterval(this.pingInterval),this.pingInterval=null)}getPing(){return this.latency}startStatsMonitor(){this.stopStatsMonitor(),this.statsInterval=setInterval(async()=>{if(!(!this.peerConnection||this._state!==d.Connected))try{let t=await this.peerConnection.getStats(),e=null;if(t.forEach(n=>{if(n.type==="transport"){let i=n.selectedCandidatePairId;e=t.get?t.get(i):t[i]}}),e){let n=t,i=n.get?n.get(e.localCandidateId):n[e.localCandidateId],s=n.get?n.get(e.remoteCandidateId):n[e.remoteCandidateId];this.options.debug&&this.log(`RTC Stats: Connection=${e.state} RTT=${e.currentRoundTripTime?Math.round(e.currentRoundTripTime*1e3):"N/A"}ms Path=${i?.candidateType||"?"}<->${s?.candidateType||"?"}`),this.p2pOnly&&(i?.candidateType==="relay"||s?.candidateType==="relay")&&(this.error("STRICT P2P VIOLATION: Detected relay path in stats. DISCONNECTING."),this.disconnect())}}catch(t){this.error("Failed to get RTC stats",t)}},5e3)}stopStatsMonitor(){this.statsInterval&&(clearInterval(this.statsInterval),this.statsInterval=null)}stripRelayFromSDP(t){return t.split(`
|
|
2
|
+
`).filter(e=>!e.includes("typ relay")).join(`
|
|
3
|
+
`)}destroy(){this.disconnect(),this.removeAllListeners()}sendBridge(t,e){this.send("bridge",{channel:t,data:e})}onBridge(t,e){this.bridgeHandlers.has(t)||this.bridgeHandlers.set(t,[]),this.bridgeHandlers.get(t).push(e)}offBridge(t,e){let n=this.bridgeHandlers.get(t);if(n){let i=n.indexOf(e);i!==-1&&n.splice(i,1)}}removeAllBridgeHandlers(t){t?this.bridgeHandlers.delete(t):this.bridgeHandlers.clear()}log(t,...e){this.options.debug&&console.warn(`[WebRTC-Client] ${t}`,...e)}parseCandidateType(t){return t?t.includes("typ host")?"host":t.includes("typ srflx")?"srflx":t.includes("typ relay")?"relay":"unknown":"unknown"}warn(t,...e){this.options.debug&&console.warn(`[WebRTC-Client] ${t}`,...e)}error(t,...e){console.error(`[WebRTC-Client] \u274C ${t}`,...e)}};f(v,"WebRTCClient");var C=v;export{U as NetworkState,y as SocketIOClient,C as WebRTCClient};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utsp/network-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0-nightly.20260204223142.cdfe4ca",
|
|
4
4
|
"description": "UTSP Network Client - Client-side communication adapters",
|
|
5
5
|
"author": "THP Software",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"access": "public"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"
|
|
52
|
-
"
|
|
51
|
+
"@utsp/types": "0.18.0-nightly.20260204223142.cdfe4ca",
|
|
52
|
+
"socket.io-client": "^4.7.2"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"typescript": "^5.6.3"
|