@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 CHANGED
@@ -1,36 +1,40 @@
1
+ <div align="center">
2
+
1
3
  # @utsp/network-client
2
4
 
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.
5
+ [![NPM Version](https://img.shields.io/npm/v/@utsp/network-client.svg)](https://www.npmjs.com/package/@utsp/network-client)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
- Client-side network communication layer for UTSP (Universal Tile Stream Protocol).
8
+ Visit [**UTSP.dev**](https://utsp.dev/) for more information.
9
9
 
10
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
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
- ## ⚠️ Development Status
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
- **This is a prototype package under active development.**
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
- - No stable API
17
- - ❌ No documentation available yet
18
- - ❌ Breaking changes expected
19
- - ❌ Not recommended for production use
24
+ ## Features
20
25
 
21
- **Please check back later for updates or watch the repository for release announcements.**
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
- ## Repository
34
+ ## 📖 Documentation
30
35
 
31
- - [GitHub](https://github.com/thp-software/utsp)
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 © 2025 THP Software
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(t=>!t.includes("typ relay")).join(`
3
- `)}destroy(){this.disconnect(),this.removeAllListeners()}sendBridge(e,t){this.send("bridge",{channel:e,data:t})}onBridge(e,t){this.bridgeHandlers.has(e)||this.bridgeHandlers.set(e,[]),this.bridgeHandlers.get(e).push(t)}offBridge(e,t){let n=this.bridgeHandlers.get(e);if(n){let i=n.indexOf(t);i!==-1&&n.splice(i,1)}}removeAllBridgeHandlers(e){e?this.bridgeHandlers.delete(e):this.bridgeHandlers.clear()}log(e,...t){this.options.debug&&console.warn(`[WebRTC-Client] ${e}`,...t)}parseCandidateType(e){return e?e.includes("typ host")?"host":e.includes("typ srflx")?"srflx":e.includes("typ relay")?"relay":"unknown":"unknown"}warn(e,...t){this.options.debug&&console.warn(`[WebRTC-Client] ${e}`,...t)}error(e,...t){console.error(`[WebRTC-Client] \u274C ${e}`,...t)}};u(k,"WebRTCClient");var y=k;0&&(module.exports={NetworkState,SocketIOClient,WebRTCClient});
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(t=>!t.includes("typ relay")).join(`
3
- `)}destroy(){this.disconnect(),this.removeAllListeners()}sendBridge(e,t){this.send("bridge",{channel:e,data:t})}onBridge(e,t){this.bridgeHandlers.has(e)||this.bridgeHandlers.set(e,[]),this.bridgeHandlers.get(e).push(t)}offBridge(e,t){let n=this.bridgeHandlers.get(e);if(n){let i=n.indexOf(t);i!==-1&&n.splice(i,1)}}removeAllBridgeHandlers(e){e?this.bridgeHandlers.delete(e):this.bridgeHandlers.clear()}log(e,...t){this.options.debug&&console.warn(`[WebRTC-Client] ${e}`,...t)}parseCandidateType(e){return e?e.includes("typ host")?"host":e.includes("typ srflx")?"srflx":e.includes("typ relay")?"relay":"unknown":"unknown"}warn(e,...t){this.options.debug&&console.warn(`[WebRTC-Client] ${e}`,...t)}error(e,...t){console.error(`[WebRTC-Client] \u274C ${e}`,...t)}};p(v,"WebRTCClient");var C=v;export{U as NetworkState,y as SocketIOClient,C as WebRTCClient};
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.17.3",
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
- "socket.io-client": "^4.7.2",
52
- "@utsp/types": "0.17.3"
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"