fastrtc 1.0.1 → 1.1.0

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
@@ -105,10 +105,13 @@ rtc.on("ready", (peerId) => {
105
105
  | ------- | ---- |
106
106
  | `join` | Peer connection created |
107
107
  | `ready` | All declared channels open for that peer (fires once) |
108
- | `leave` | Peer disconnected |
108
+ | `leave` | Peer disconnected (including ICE `disconnected`) |
109
+ | `error` | Signaling or negotiation failed for a peer |
109
110
 
110
111
  Get a channel directly: `rtc.getChannel(peerId, "files")` or `await rtc.whenOpen(peerId, "files")`.
111
112
 
113
+ **Late channels:** `ready` fires once per peer when all channels declared *at that moment* are open. If you call `channel()` after `ready`, use `whenOpen(peerId, label)` for the new label. On the polite side (lexicographically larger ID), only the impolite peer creates channels — register labels on both sides before `connect()`, or the polite peer will never open them alone.
114
+
112
115
  ### Sending
113
116
 
114
117
  | Method | Target |
@@ -141,13 +144,13 @@ Strings are drained before queued file frames, so control messages are not stuck
141
144
 
142
145
  ### Receiving
143
146
 
144
- Attach a `stream` listener before data arrives — streams without a listener are dropped. A slow consumer that lets the receive queue grow past the backlog limit gets an error instead of unbounded RAM.
147
+ Attach a `stream` listener before data arrives — streams without a listener are dropped. A slow consumer that lets the receive queue grow past the backlog limit gets an error instead of unbounded RAM. If the sender aborts or the channel closes mid-transfer, the receive stream errors (not a clean close).
145
148
 
146
149
  ## Media
147
150
 
148
151
  ```ts
149
152
  const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
150
- rtc.add(stream); // tracks added before connect() attach to new peers automatically
153
+ rtc.add(stream); // tracks on the stream at add-time attach to current and future peers
151
154
 
152
155
  rtc.on("track", (peerId, track, stream) => {
153
156
  videoElement.srcObject = stream;
@@ -158,6 +161,8 @@ rtc.on("track-ended", (peerId, track) => { /* remote track stopped */ });
158
161
  rtc.remove(stream);
159
162
  ```
160
163
 
164
+ Tracks added to a `MediaStream` *after* `add()` are not auto-forwarded. Call `remove(stream)` then `add(stream)`, or add tracks before `add()`.
165
+
161
166
  ## Transfer limiter
162
167
 
163
168
  Large file sends use memory. `maxTransfers` caps how many run at once (default `4`); extras wait in FIFO.
@@ -200,7 +205,7 @@ new RTCManager({
200
205
  | `id` | Local peer ID |
201
206
  | `rtcConfig` | Active RTC configuration |
202
207
 
203
- **Events:** `join`, `ready`, `leave`, `channel`, `track`, `track-ended`. `.on(event, handler)` returns unsubscribe.
208
+ **Events:** `join`, `ready`, `leave`, `channel`, `track`, `track-ended`, `error`. `.on(event, handler)` returns unsubscribe.
204
209
 
205
210
  ### `FastDataChannel`
206
211
 
package/dist/consts.d.ts CHANGED
@@ -2,6 +2,7 @@ export declare const FRAME_MAGIC = 252;
2
2
  export declare const FLAG_FIRST = 1;
3
3
  export declare const FLAG_LAST = 2;
4
4
  export declare const FLAG_META = 4;
5
+ export declare const FLAG_ABORT = 8;
5
6
  export declare const FRAME_HEADER_SIZE = 2;
6
7
  export declare const RECEIVE_BACKLOG = 32;
7
8
  export declare const SCTP_DEFAULT_MESSAGE_SIZE = 65536;
@@ -17,7 +17,7 @@ interface FastDataChannelEvents {
17
17
  export declare class FastDataChannel extends EventEmitter<FastDataChannelEvents> {
18
18
  #private;
19
19
  readonly channel: RTCDataChannel;
20
- constructor(channel: RTCDataChannel, transport: RTCSctpTransport | RTCPeerConnection | null, limiter?: TransferLimiter);
20
+ constructor(channel: RTCDataChannel, pc: RTCPeerConnection | null, limiter?: TransferLimiter);
21
21
  send(data: SendData, options?: SendOptions): Promise<void>;
22
22
  destroy(): void;
23
23
  }
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var R=Object.defineProperty;var N=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var I=(i,e)=>{for(var t in e)R(i,t,{get:e[t],enumerable:!0})},q=(i,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of j(e))!K.call(i,r)&&r!==t&&R(i,r,{get:()=>e[r],enumerable:!(n=N(e,r))||n.enumerable});return i};var V=i=>q(R({},"__esModule",{value:!0}),i);var H={};I(H,{FastDataChannel:()=>w,RTCManager:()=>k});module.exports=V(H);var F={iceServers:[{urls:"stun:stun.l.google.com:19302"}]},L=4;var p=class{#t=new Map;on(e,t){let n=t,r=this.#t.get(e)??[];return r.push(n),this.#t.set(e,r),()=>{let s=this.#t.get(e);if(!s)return;let a=s.indexOf(n);a>=0&&s.splice(a,1),s.length===0&&this.#t.delete(e)}}has(e){return(this.#t.get(e)?.length??0)>0}emit(e,...t){let n=this.#t.get(e);if(n)for(let r of n.slice())r(...t)}destroy(){this.#t.clear()}};function B(i,e){let t=new ArrayBuffer(2+e.byteLength),n=new DataView(t);return n.setUint8(0,252),n.setUint8(1,i),new Uint8Array(t,2).set(e),t}function U(i){if(i.byteLength<2)return null;let e=new DataView(i);return e.getUint8(0)!==252?null:{flags:e.getUint8(1),payload:new Uint8Array(i,2)}}function _(i){return i instanceof Blob||i instanceof ArrayBuffer||ArrayBuffer.isView(i)}function O(i){if(i instanceof Blob)return i.stream();if(i instanceof ArrayBuffer||ArrayBuffer.isView(i)){let t=T(i);return new ReadableStream({start(n){n.enqueue(t),n.close()}})}if(i instanceof ReadableStream)return i;let e=i[Symbol.asyncIterator]();return new ReadableStream({async pull(t){let{done:n,value:r}=await e.next();if(n){t.close();return}t.enqueue(T(r))},cancel:()=>{e.return?.()}})}function T(i){if(i instanceof Uint8Array)return i;if(i instanceof ArrayBuffer)return new Uint8Array(i);if(ArrayBuffer.isView(i))return new Uint8Array(i.buffer,i.byteOffset,i.byteLength);if(typeof i=="string")return new TextEncoder().encode(i);throw i instanceof Blob?new Error("Pass the Blob/File to send, not blob chunks"):new Error("Unsupported stream chunk")}var S=class{#t;#e=0;#s=[];#i=!1;constructor(e){this.#t=e}async acquire(){if(this.#i)throw new Error("Transfer limiter destroyed");if(this.#e<this.#t)this.#e++;else{let t=Promise.withResolvers();if(this.#s.push(t),await t.promise,this.#i)throw this.#e--,new Error("Transfer limiter destroyed")}let e=!1;return()=>{e||(e=!0,this.#n())}}destroy(){this.#i=!0;let e=new Error("Transfer limiter destroyed");for(let t of this.#s.splice(0))t.reject(e)}#n(){if(this.#i){this.#e--;return}let e=this.#s.shift();if(e){e.resolve();return}this.#e--}};function Z(i){let e=i?.maxMessageSize;return!e||!Number.isFinite(e)?65536:e}var w=class extends p{channel;#t=0;#e=null;#s=null;#i;#n=[];#r=null;#a=null;#o=!1;#l=!1;#u=Promise.resolve();#d=0;constructor(e,t,n=new S(1/0)){super(),this.channel=e,this.#i=n,t instanceof RTCPeerConnection?this.#e=t:this.#s=t,this.#f(),this.channel.binaryType="arraybuffer",this.channel.onopen=()=>this.#m(),this.channel.onclose=()=>this.destroy(),this.channel.onmessage=r=>this.#C(r.data),this.channel.onerror=r=>this.emit("error",r),this.channel.onbufferedamountlow=()=>{this.#h(),this.#p()},this.channel.readyState==="open"&&queueMicrotask(()=>this.#m())}async send(e,t={}){if(this.#l)throw new Error("Data channel closed");if(typeof e=="string"){this.#n.push(e),this.#h();return}await this.#v(async()=>{let n=await this.#i.acquire();try{await this.#b(e,t)}finally{n()}})}destroy(){if(this.#l)return;this.#l=!0,this.#n.length=0,this.#o=!1;let e=new Error("Data channel closed");this.#r?.reject(e),this.#r=null,this.#a?.error(e),this.#a=null,this.emit("close"),this.channel.onopen=null,this.channel.onclose=null,this.channel.onmessage=null,this.channel.onerror=null,this.channel.onbufferedamountlow=null,super.destroy();try{this.channel.close()}catch{}}#m(){this.#l||(this.#f(),this.#h(),this.emit("open"))}#f(){let e=Z(this.#s??this.#e?.sctp??null);this.#t=Math.max(1,e-2),this.channel.bufferedAmountLowThreshold=e*2}async#v(e){let t=this.#u,{promise:n,resolve:r}=Promise.withResolvers();this.#u=n,await t;try{await e()}finally{r()}}#S(e,t){if(e instanceof Blob)return e.size-t;if(e instanceof ArrayBuffer||ArrayBuffer.isView(e))return e.byteLength-t}#w(e,t,n){n?.(e,t),this.emit("progress",{direction:"send",bytes:e,total:t})}async#b(e,t){this.#f();let n=t.offset??0;if(n!==0&&!(e instanceof Blob))throw new Error("offset requires a Blob/File");let r=e;n!==0&&e instanceof Blob&&(r=e.slice(n));let s=this.#S(e,n),a=0,c=!1;if(t.meta!==void 0){let l=new TextEncoder().encode(JSON.stringify(t.meta));this.#c(5,l),c=!0}let m=O(r),u=m.getReader(),h=!c,d=null,b=()=>{if(t.signal?.aborted)throw t.signal.reason??new Error("Aborted");if(this.#l)throw new Error("Data channel closed")},y=l=>{if(!d)return;let g=d.payload;this.#c(d.flags|(l?2:0),g),d=null,a+=g.byteLength,this.#w(a,s,t.onProgress)},o=()=>{u.cancel(t.signal?.reason??new Error("Aborted"))};t.signal?.addEventListener("abort",o,{once:!0});try{for(;;){b(),await this.#g(t.signal);let{done:l,value:g}=await u.read();if(l){d?y(!0):h?this.#c(3,new Uint8Array(0)):c&&this.#c(2,new Uint8Array(0));break}let D=T(g),C=0;for(;C<D.byteLength;){b(),await this.#g(t.signal);let P=Math.min(C+this.#t,D.byteLength);y(!1);let G=h?1:0;h=!1,d={flags:G,payload:D.subarray(C,P)},C=P}}}catch(l){try{d?y(!0):h||this.#c(2,new Uint8Array(0))}catch{}try{await u.cancel(l)}catch{}throw l}finally{t.signal?.removeEventListener("abort",o),m.locked&&u.releaseLock()}}#C(e){let t=e instanceof ArrayBuffer?U(e):null;if(!t){this.emit("message",e);return}if(this.#o){t.flags&2&&(this.#o=!1);return}if(!this.#a){if(!(t.flags&1))return;if(!this.has("stream")){this.#o=!(t.flags&2);return}let r;if(t.flags&4)try{r=JSON.parse(new TextDecoder().decode(t.payload))}catch(c){this.emit("error",c),this.#o=!(t.flags&2);return}let s,a=new ReadableStream({start:c=>{s=c,this.#a=c},cancel:()=>{this.#a=null}});if(this.#d=0,this.emit("stream",a,r),t.flags&4){t.flags&2&&(s?.close(),this.#a=null);return}}let n=this.#a;if(n){if(n.enqueue(t.payload),this.#d+=t.payload.byteLength,this.emit("progress",{direction:"receive",bytes:this.#d}),(n.desiredSize??0)<-32){n.error(new Error("Receive stream backlog exceeded")),this.#a=null,this.#o=!(t.flags&2);return}t.flags&2&&(n.close(),this.#a=null)}}#c(e,t){this.#l||(this.#n.push(B(e,t)),this.#h())}#y(){return!this.#l&&this.channel.readyState==="open"&&this.#n.length===0&&this.channel.bufferedAmount<=this.channel.bufferedAmountLowThreshold}#T(){let e=this.#n.findIndex(t=>typeof t=="string");return e>=0?this.#n.splice(e,1)[0]:this.#n.shift()}#h(){if(!(this.#l||this.channel.readyState!=="open")){for(;this.#n.length>0&&!(this.channel.bufferedAmount>this.channel.bufferedAmountLowThreshold);){let e=this.#T();if(e===void 0)break;try{typeof e=="string"?this.channel.send(e):this.channel.send(e)}catch(t){this.emit("error",t),this.#n.unshift(e);break}}this.#p()}}#g(e){if(this.#l)return Promise.reject(new Error("Data channel closed"));if(e?.aborted)return Promise.reject(e.reason??new Error("Aborted"));if(this.#y())return Promise.resolve();this.#r||(this.#r=Promise.withResolvers());let{promise:t}=this.#r,n=()=>{this.#r?.reject(e?.reason??new Error("Aborted")),this.#r=null};return e?.addEventListener("abort",n,{once:!0}),t.finally(()=>{e?.removeEventListener("abort",n)})}#p(){this.#y()&&(this.#r?.resolve(),this.#r=null)}};var E=class{remoteId;channels=new Map;get isConnected(){return this.#e.connectionState==="connected"}#t;#e;#s;#i=[];#n=!1;#r=!1;constructor(e){this.remoteId=e.remoteId,this.#t=e,this.#s=e.localId>e.remoteId,this.#e=new RTCPeerConnection(e.rtcConfig),this.#e.onicecandidate=t=>{t.candidate&&this.#t.onSignal({type:"candidate",candidate:t.candidate})},this.#e.onnegotiationneeded=async()=>{try{this.#n=!0,await this.#e.setLocalDescription(),this.#t.onSignal({type:"description",description:this.#e.localDescription})}catch{}finally{this.#n=!1}},this.#e.onconnectionstatechange=()=>{this.#e.connectionState==="connected"&&this.#t.onConnected(),["disconnected","failed","closed"].includes(this.#e.connectionState)&&this.#t.onDisconnect()},this.#e.ontrack=t=>{let n=t.streams[0];t.track.onended=()=>this.#t.onTrackEnded(t.track,n),this.#t.onTrack(t.track,n)},this.#e.ondatachannel=t=>this.#a(t.channel)}addTrack(e,t){this.#e.addTrack(e,t)}removeTrack(e){let t=this.#e.getSenders().find(n=>n.track===e);if(t)try{this.#e.removeTrack(t)}catch{}}async receiveSignal(e){try{if(e.type==="description"){let t=e.description.type==="offer"&&(this.#n||this.#e.signalingState!=="stable");if(this.#r=!this.#s&&t,this.#r)return;await this.#e.setRemoteDescription(e.description);for(let n of this.#i)await this.#e.addIceCandidate(n);this.#i=[],e.description.type==="offer"&&(await this.#e.setLocalDescription(),this.#t.onSignal({type:"description",description:this.#e.localDescription}));return}this.#e.remoteDescription?await this.#e.addIceCandidate(e.candidate):this.#i.push(e.candidate)}catch{}}createDataChannel(e,t){this.#s||this.#a(this.#e.createDataChannel(e,t))}#a(e){this.channels.get(e.label)?.destroy();let n=new w(e,this.#e,this.#t.limiter);this.channels.set(e.label,n),this.#t.onChannel(n)}destroy(){this.#e.onicecandidate=null,this.#e.onnegotiationneeded=null,this.#e.onconnectionstatechange=null,this.#e.ontrack=null,this.#e.ondatachannel=null;for(let e of this.#e.getReceivers())e.track.onended=null;for(let e of this.channels.values())e.destroy();this.channels.clear();try{this.#e.close()}catch{}}};var k=class extends p{id;rtcConfig;#t=new Map;#e;#s=new Set;#i=new Map;#n;#r=new Set;constructor({id:e,signal:t,rtcConfig:n=F,maxTransfers:r=L}){super(),this.id=e,this.#e=t,this.rtcConfig=n,this.#n=new S(r)}add(e){this.#s.add(e);for(let t of e.getTracks())for(let n of this.#t.values())n.addTrack(t,e)}remove(e){this.#s.delete(e);for(let t of e.getTracks())for(let n of this.#t.values())n.removeTrack(t)}channel(e,t={}){this.#i.set(e,t);for(let n of this.#t.values())n.createDataChannel(e,t)}getChannel(e,t){return this.#t.get(e)?.channels.get(t)}whenOpen(e,t,n={}){let r=this.getChannel(e,t);if(r?.channel.readyState==="open")return Promise.resolve(r);let{promise:s,resolve:a,reject:c}=Promise.withResolvers(),m=[],u=!1,h=(o,l)=>{if(!u){u=!0;for(let g of m)g();n.signal?.removeEventListener("abort",d),l?a(l):c(o??new Error("Channel open failed"))}},d=()=>{h(n.signal?.reason??new Error("Aborted"))};if(n.signal?.aborted)return Promise.reject(n.signal.reason??new Error("Aborted"));n.signal?.addEventListener("abort",d,{once:!0});let b=o=>{if(o.channel.readyState==="open"){h(void 0,o);return}m.push(o.on("open",()=>h(void 0,o)),o.on("close",()=>h(new Error("Data channel closed"))))},y=this.getChannel(e,t);return y&&b(y),m.push(this.on("channel",(o,l)=>{o!==e||l.channel.label!==t||b(l)}),this.on("leave",o=>{o===e&&h(new Error(`Peer left: ${e}`))})),s}connect(e){if(this.#t.has(e))return;let t=new E({localId:this.id,remoteId:e,rtcConfig:this.rtcConfig,limiter:this.#n,onSignal:n=>this.#e(e,n),onTrack:(n,r)=>this.emit("track",e,n,r),onTrackEnded:(n,r)=>this.emit("track-ended",e,n,r),onChannel:n=>{this.emit("channel",e,n);let r=()=>this.#o(e);n.on("open",r),n.channel.readyState==="open"&&r()},onConnected:()=>this.#o(e),onDisconnect:()=>{this.#t.delete(e)&&(this.#r.delete(e),t.destroy(),this.emit("leave",e))}});this.#i.forEach((n,r)=>t.createDataChannel(r,n));for(let n of this.#s)for(let r of n.getTracks())t.addTrack(r,n);this.#t.set(e,t),this.emit("join",e),this.#o(e)}receive(e,t){this.connect(e),this.#t.get(e).receiveSignal(t)}async sendTo(e,t,n,r={}){let s=this.#t.get(e);if(!s)throw new Error(`Unknown peer: ${e}`);let a=s.channels.get(t);if(!a)throw new Error(`No channel "${t}" for peer ${e}`);if(a.channel.readyState!=="open")throw new Error(`Channel "${t}" with ${e} is not open`);await a.send(n,r)}async send(e,t,n={}){let r=[];for(let s of this.#t.values()){let a=s.channels.get(e);a?.channel.readyState==="open"&&r.push(a)}if(r.length!==0){if(r.length>1&&typeof t!="string"&&!_(t))throw new Error("Pass a Blob/File/BufferSource to send to multiple peers");await Promise.all(r.map(s=>s.send(t,n)))}}close(e){let t=this.#t.get(e);t&&(this.#t.delete(e),this.#r.delete(e),t.destroy(),this.emit("leave",e))}destroy(){this.#n.destroy();for(let e of[...this.#t.keys()])this.close(e);this.#s.clear(),this.#i.clear(),this.#r.clear(),super.destroy()}#a(e){let t=this.#t.get(e);if(!t)return!1;if(this.#i.size===0)return t.isConnected;for(let n of this.#i.keys()){let r=t.channels.get(n);if(!r||r.channel.readyState!=="open")return!1}return!0}#o(e){this.#r.has(e)||this.#a(e)&&(this.#r.add(e),this.emit("ready",e))}};0&&(module.exports={FastDataChannel,RTCManager});
1
+ "use strict";var P=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var K=Object.getOwnPropertyNames;var z=Object.prototype.hasOwnProperty;var V=(r,e)=>{for(var t in e)P(r,t,{get:e[t],enumerable:!0})},$=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of K(e))!z.call(r,i)&&i!==t&&P(r,i,{get:()=>e[i],enumerable:!(n=I(e,i))||n.enumerable});return r};var Q=r=>$(P({},"__esModule",{value:!0}),r);var W={};V(W,{FastDataChannel:()=>v,RTCManager:()=>R});module.exports=Q(W);var B={iceServers:[{urls:"stun:stun.l.google.com:19302"}]},_=4;var p=class{#t=new Map;on(e,t){let n=t,i=this.#t.get(e)??[];return i.push(n),this.#t.set(e,i),()=>{let a=this.#t.get(e);if(!a)return;let o=a.indexOf(n);o>=0&&a.splice(o,1),a.length===0&&this.#t.delete(e)}}has(e){return(this.#t.get(e)?.length??0)>0}emit(e,...t){let n=this.#t.get(e);if(n)for(let i of n.slice())i(...t)}destroy(){this.#t.clear()}};function A(r,e){let t=new ArrayBuffer(2+e.byteLength),n=new DataView(t);return n.setUint8(0,252),n.setUint8(1,r),new Uint8Array(t,2).set(e),t}function O(r){if(r.byteLength<2)return null;let e=new DataView(r);return e.getUint8(0)!==252?null:{flags:e.getUint8(1),payload:new Uint8Array(r,2)}}function T(){let r=Promise.resolve();return e=>{let t=r.then(e,e);return r=t.then(()=>{},()=>{}),t}}function G(r){return r instanceof Blob||r instanceof ArrayBuffer||ArrayBuffer.isView(r)}function j(r){if(r instanceof Blob)return r.stream();if(r instanceof ArrayBuffer||ArrayBuffer.isView(r)){let t=k(r);return new ReadableStream({start(n){n.enqueue(t),n.close()}})}if(r instanceof ReadableStream)return r;let e=r[Symbol.asyncIterator]();return new ReadableStream({async pull(t){let{done:n,value:i}=await e.next();if(n){t.close();return}t.enqueue(k(i))},cancel:()=>{e.return?.()}})}function k(r){if(r instanceof Uint8Array)return r;if(r instanceof ArrayBuffer)return new Uint8Array(r);if(ArrayBuffer.isView(r))return new Uint8Array(r.buffer,r.byteOffset,r.byteLength);if(typeof r=="string")return new TextEncoder().encode(r);throw r instanceof Blob?new Error("Pass the Blob/File to send, not blob chunks"):new Error("Unsupported stream chunk")}var w=class{#t;#e=0;#o=[];#n=!1;constructor(e){if(e<=0&&e!==1/0)throw new Error("max must be positive or Infinity");this.#t=e}async acquire(){if(this.#n)throw new Error("Transfer limiter destroyed");if(this.#e<this.#t)this.#e++;else{let t=Promise.withResolvers();if(this.#o.push(t),await t.promise,this.#n)throw this.#e--,new Error("Transfer limiter destroyed")}let e=!1;return()=>{e||(e=!0,this.#r())}}destroy(){this.#n=!0;let e=new Error("Transfer limiter destroyed");for(let t of this.#o.splice(0))t.reject(e)}#r(){if(this.#n){this.#e--;return}let e=this.#o.shift();if(e){e.resolve();return}this.#e--}};function J(r){let e=r?.maxMessageSize;return!e||!Number.isFinite(e)?65536:e}var v=class extends p{channel;#t=0;#e=null;#o;#n=[];#r=null;#a=null;#i=!1;#s=!1;#h=!1;#u=T();#l=0;constructor(e,t,n=new w(1/0)){super(),this.channel=e,this.#e=t,this.#o=n,this.#m(),this.channel.binaryType="arraybuffer",this.channel.onopen=()=>this.#g(),this.channel.onclose=()=>this.destroy(),this.channel.onmessage=i=>this.#T(i.data),this.channel.onerror=i=>this.emit("error",i),this.channel.onbufferedamountlow=()=>{this.#c(),this.#S()},this.channel.readyState==="open"&&queueMicrotask(()=>this.#g())}async send(e,t={}){if(this.#s)throw new Error("Data channel closed");if(typeof e=="string"){this.#p(new TextEncoder().encode(e).byteLength),this.#n.push(e),this.#c();return}await this.#u(async()=>{let n=await this.#o.acquire();try{this.#h=!0,await this.#A(e,t)}finally{this.#h=!1,n()}})}destroy(){if(this.#s)return;this.#h&&(this.#n.push(A(10,new Uint8Array(0))),this.#c()),this.#s=!0,this.#n.length=0,this.#d(new Error("Data channel closed"));let e=new Error("Data channel closed");this.#r?.reject(e),this.#r=null,this.emit("close"),this.channel.onopen=null,this.channel.onclose=null,this.channel.onmessage=null,this.channel.onerror=null,this.channel.onbufferedamountlow=null,super.destroy();try{this.channel.close()}catch{}}#g(){this.#s||(this.#m(),this.#c(),this.emit("open"))}#y(){return J(this.#e?.sctp??null)}#m(){let e=this.#y();this.#t=Math.max(1,e-2),this.channel.bufferedAmountLowThreshold=e*2}#p(e){let t=this.#y();if(e>t)throw new Error(`Message exceeds max SCTP size (${t})`)}#d(e){let t=this.#a;if(t){try{t.error(e)}catch{}this.#a=null}this.#i=!0}#b(){this.#s||this.channel.readyState!=="open"||(this.#n.push(A(10,new Uint8Array(0))),this.#c())}#C(e,t){if(e instanceof Blob)return e.size-t;if(e instanceof ArrayBuffer||ArrayBuffer.isView(e))return e.byteLength-t}#E(e,t,n){n?.(e,t),this.emit("progress",{direction:"send",bytes:e,total:t})}async#A(e,t){this.#m();let n=t.offset??0;if(n!==0&&!(e instanceof Blob))throw new Error("offset requires a Blob/File");let i=e;n!==0&&e instanceof Blob&&(i=e.slice(n));let a=this.#C(e,n),o=0,c=!1;if(t.meta!==void 0){let s=new TextEncoder().encode(JSON.stringify(t.meta));this.#f(5,s),c=!0}let g=j(i),u=g.getReader(),d=!c,f=null,S=()=>{if(t.signal?.aborted)throw t.signal.reason??new Error("Aborted");if(this.#s)throw new Error("Data channel closed")},y=s=>{if(!f)return;let h=f.payload;this.#f(f.flags|(s?2:0),h),f=null,o+=h.byteLength,this.#E(o,a,t.onProgress)},b=()=>{u.cancel(t.signal?.reason??new Error("Aborted"))};t.signal?.addEventListener("abort",b,{once:!0});try{for(;;){S(),await this.#v(t.signal);let{done:s,value:h}=await u.read();if(s){f?y(!0):d?this.#f(3,new Uint8Array(0)):c&&this.#f(2,new Uint8Array(0));break}let C=k(h),E=0;for(;E<C.byteLength;){S(),await this.#v(t.signal);let L=Math.min(E+this.#t,C.byteLength);y(!1);let N=d?1:0;d=!1,f={flags:N,payload:C.slice(E,L)},E=L}}}catch(s){try{this.#b()}catch{}try{await u.cancel(s)}catch{}throw s}finally{t.signal?.removeEventListener("abort",b),g.locked&&u.releaseLock()}}#T(e){let t=e instanceof ArrayBuffer?O(e):null;if(!t){this.emit("message",e);return}if(t.flags&8){this.#d(new Error("Stream aborted")),t.flags&2&&(this.#i=!1);return}if(this.#i){t.flags&2&&(this.#i=!1);return}if(!this.#a){if(!(t.flags&1))return;if(!this.has("stream")){this.#i=!(t.flags&2);return}let i;if(t.flags&4)try{i=JSON.parse(new TextDecoder().decode(t.payload))}catch(c){this.emit("error",c),this.#i=!(t.flags&2);return}let a,o=new ReadableStream({start:c=>{a=c,this.#a=c},cancel:()=>{this.#d(new Error("Stream cancelled"))}});if(this.#l=0,this.emit("stream",o,i),t.flags&4){t.flags&2&&(a?.close(),this.#a=null);return}}let n=this.#a;if(n){if(n.enqueue(t.payload),this.#l+=t.payload.byteLength,this.emit("progress",{direction:"receive",bytes:this.#l}),(n.desiredSize??0)<-32){if(this.#d(new Error("Receive stream backlog exceeded")),!(t.flags&2))return;this.#i=!1;return}t.flags&2&&(n.close(),this.#a=null)}}#f(e,t){this.#s||(this.#p(2+t.byteLength),this.#n.push(A(e,t)),this.#c())}#w(){return!this.#s&&this.channel.readyState==="open"&&this.#n.length===0&&this.channel.bufferedAmount<=this.channel.bufferedAmountLowThreshold}#k(){let e=this.#n.findIndex(t=>typeof t=="string");return e>=0?this.#n.splice(e,1)[0]:this.#n.shift()}#c(){if(!(this.#s||this.channel.readyState!=="open")){for(;this.#n.length>0&&!(this.channel.bufferedAmount>this.channel.bufferedAmountLowThreshold);){let e=this.#k();if(e===void 0)break;try{typeof e=="string"?this.channel.send(e):this.channel.send(e)}catch(t){this.emit("error",t),this.#n.unshift(e);break}}this.#S()}}#v(e){if(this.#s)return Promise.reject(new Error("Data channel closed"));if(e?.aborted)return Promise.reject(e.reason??new Error("Aborted"));if(this.#w())return Promise.resolve();this.#r||(this.#r=Promise.withResolvers());let{promise:t}=this.#r,n=()=>{this.#r?.reject(e?.reason??new Error("Aborted")),this.#r=null};return e?.addEventListener("abort",n,{once:!0}),t.finally(()=>{e?.removeEventListener("abort",n)})}#S(){this.#w()&&(this.#r?.resolve(),this.#r=null)}};function q(r){return r instanceof DOMException&&r.name==="InvalidStateError"}var x=class{remoteId;channels=new Map;get isConnected(){return this.#e.connectionState==="connected"}#t;#e;#o;#n=[];#r=!1;#a=!1;#i=!1;#s=T();constructor(e){this.remoteId=e.remoteId,this.#t=e,this.#o=e.localId>e.remoteId,this.#e=new RTCPeerConnection(e.rtcConfig),this.#e.onicecandidate=t=>{this.#i||!t.candidate||this.#t.onSignal({type:"candidate",candidate:t.candidate})},this.#e.onnegotiationneeded=()=>{this.#s(async()=>{if(!this.#i)try{this.#r=!0,await this.#e.setLocalDescription(),!this.#i&&this.#e.localDescription&&this.#t.onSignal({type:"description",description:this.#e.localDescription})}catch(t){q(t)||this.#t.onError(t)}finally{this.#r=!1}})},this.#e.onconnectionstatechange=()=>{this.#e.connectionState==="connected"&&this.#t.onConnected(),["disconnected","failed","closed"].includes(this.#e.connectionState)&&this.#t.onDisconnect()},this.#e.ontrack=t=>{let n=t.streams[0]??new MediaStream([t.track]);t.track.onended=()=>this.#t.onTrackEnded(t.track,n),this.#t.onTrack(t.track,n)},this.#e.ondatachannel=t=>this.#h(t.channel)}addTrack(e,t){this.#e.getSenders().some(n=>n.track===e)||this.#e.addTrack(e,t)}removeTrack(e){let t=this.#e.getSenders().find(n=>n.track===e);if(t)try{this.#e.removeTrack(t)}catch{}}receiveSignal(e){this.#s(async()=>{if(!this.#i)try{if(e.type==="description"){let t=e.description.type==="offer"&&(this.#r||this.#e.signalingState!=="stable");if(this.#a=!this.#o&&t,this.#a)return;await this.#e.setRemoteDescription(e.description);for(let n of this.#n)await this.#e.addIceCandidate(n);this.#n=[],e.description.type==="offer"&&(await this.#e.setLocalDescription(),!this.#i&&this.#e.localDescription&&this.#t.onSignal({type:"description",description:this.#e.localDescription}));return}if(this.#a)return;this.#e.remoteDescription?await this.#e.addIceCandidate(e.candidate):this.#n.push(e.candidate)}catch(t){!this.#a&&!q(t)&&this.#t.onError(t)}})}createDataChannel(e,t){this.#o||this.#h(this.#e.createDataChannel(e,t))}#h(e){let t=this.channels.get(e.label),n=new v(e,this.#e,this.#t.limiter);this.channels.set(e.label,n),t?.destroy(),this.#t.onChannel(n)}destroy(){if(!this.#i){this.#i=!0,this.#e.onicecandidate=null,this.#e.onnegotiationneeded=null,this.#e.onconnectionstatechange=null,this.#e.ontrack=null,this.#e.ondatachannel=null;for(let e of this.#e.getReceivers())e.track.onended=null;for(let e of this.channels.values())e.destroy();this.channels.clear();try{this.#e.close()}catch{}}}};var R=class extends p{id;rtcConfig;#t=new Map;#e;#o=new Set;#n=new Map;#r;#a=new Set;#i=!1;#s=new Set;constructor({id:e,signal:t,rtcConfig:n=B,maxTransfers:i=_}){super(),this.id=e,this.#e=t,this.rtcConfig=n,this.#r=new w(i)}add(e){if(this.#h(),!this.#o.has(e)){this.#o.add(e);for(let t of e.getTracks())for(let n of this.#t.values())n.addTrack(t,e)}}remove(e){this.#h(),this.#o.delete(e);for(let t of e.getTracks())for(let n of this.#t.values())n.removeTrack(t)}channel(e,t={}){this.#h(),this.#n.set(e,t);for(let n of this.#t.values())n.createDataChannel(e,t)}getChannel(e,t){return this.#t.get(e)?.channels.get(t)}whenOpen(e,t,n={}){this.#h();let i=this.getChannel(e,t);if(i?.channel.readyState==="open")return Promise.resolve(i);let{promise:a,resolve:o,reject:c}=Promise.withResolvers(),g=[],u=!1,d=(s,h)=>{if(!u){u=!0,this.#s.delete(f);for(let C of g)C();n.signal?.removeEventListener("abort",S),h?o(h):c(s??new Error("Channel open failed"))}},f=s=>{d(s)};this.#s.add(f);let S=()=>{d(n.signal?.reason??new Error("Aborted"))};if(n.signal?.aborted)return this.#s.delete(f),Promise.reject(n.signal.reason??new Error("Aborted"));n.signal?.addEventListener("abort",S,{once:!0});let y=s=>{if(s.channel.readyState==="open"){d(void 0,s);return}g.push(s.on("open",()=>d(void 0,s)),s.on("close",()=>{let h=this.getChannel(e,t);if(h&&h!==s){y(h);return}d(new Error("Data channel closed"))}))},b=this.getChannel(e,t);return b&&y(b),g.push(this.on("channel",(s,h)=>{s!==e||h.channel.label!==t||y(h)}),this.on("leave",s=>{s===e&&d(new Error(`Peer left: ${e}`))})),a}connect(e){if(this.#h(),e===this.id)throw new Error("Cannot connect to self");if(this.#t.has(e))return;let t=new x({localId:this.id,remoteId:e,rtcConfig:this.rtcConfig,limiter:this.#r,onSignal:n=>this.#e(e,n),onError:n=>this.emit("error",e,n),onTrack:(n,i)=>this.emit("track",e,n,i),onTrackEnded:(n,i)=>this.emit("track-ended",e,n,i),onChannel:n=>{this.emit("channel",e,n);let i=()=>this.#l(e);n.on("open",i),n.channel.readyState==="open"&&i()},onConnected:()=>this.#l(e),onDisconnect:()=>{this.#t.delete(e)&&(this.#a.delete(e),t.destroy(),this.emit("leave",e))}});this.#n.forEach((n,i)=>t.createDataChannel(i,n));for(let n of this.#o)for(let i of n.getTracks())t.addTrack(i,n);this.#t.set(e,t),this.emit("join",e),this.#l(e)}receive(e,t){this.#h(),this.connect(e),this.#t.get(e).receiveSignal(t)}async sendTo(e,t,n,i={}){this.#h();let a=this.#t.get(e);if(!a)throw new Error(`Unknown peer: ${e}`);let o=a.channels.get(t);if(!o)throw new Error(`No channel "${t}" for peer ${e}`);if(o.channel.readyState!=="open")throw new Error(`Channel "${t}" with ${e} is not open`);await o.send(n,i)}async send(e,t,n={}){this.#h();let i=[];for(let a of this.#t.values()){let o=a.channels.get(e);o?.channel.readyState==="open"&&i.push(o)}if(i.length!==0){if(i.length>1&&typeof t!="string"&&!G(t))throw new Error("Pass a Blob/File/BufferSource to send to multiple peers");await Promise.all(i.map(a=>a.send(t,n)))}}close(e){let t=this.#t.get(e);t&&(this.#t.delete(e),this.#a.delete(e),t.destroy(),this.emit("leave",e))}destroy(){if(this.#i)return;this.#i=!0;let e=new Error("RTCManager destroyed");for(let t of this.#s)t(e);this.#s.clear(),this.#r.destroy();for(let t of[...this.#t.keys()])this.close(t);this.#o.clear(),this.#n.clear(),this.#a.clear(),super.destroy()}#h(){if(this.#i)throw new Error("RTCManager destroyed")}#u(e){let t=this.#t.get(e);if(!t)return!1;if(this.#n.size===0)return t.isConnected;for(let n of this.#n.keys()){let i=t.channels.get(n);if(!i||i.channel.readyState!=="open")return!1}return!0}#l(e){this.#a.has(e)||this.#u(e)&&(this.#a.add(e),this.emit("ready",e))}};0&&(module.exports={FastDataChannel,RTCManager});
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- var P={iceServers:[{urls:"stun:stun.l.google.com:19302"}]},F=4;var p=class{#t=new Map;on(e,t){let n=t,r=this.#t.get(e)??[];return r.push(n),this.#t.set(e,r),()=>{let s=this.#t.get(e);if(!s)return;let a=s.indexOf(n);a>=0&&s.splice(a,1),s.length===0&&this.#t.delete(e)}}has(e){return(this.#t.get(e)?.length??0)>0}emit(e,...t){let n=this.#t.get(e);if(n)for(let r of n.slice())r(...t)}destroy(){this.#t.clear()}};function M(i,e){let t=new ArrayBuffer(2+e.byteLength),n=new DataView(t);return n.setUint8(0,252),n.setUint8(1,i),new Uint8Array(t,2).set(e),t}function B(i){if(i.byteLength<2)return null;let e=new DataView(i);return e.getUint8(0)!==252?null:{flags:e.getUint8(1),payload:new Uint8Array(i,2)}}function U(i){return i instanceof Blob||i instanceof ArrayBuffer||ArrayBuffer.isView(i)}function _(i){if(i instanceof Blob)return i.stream();if(i instanceof ArrayBuffer||ArrayBuffer.isView(i)){let t=T(i);return new ReadableStream({start(n){n.enqueue(t),n.close()}})}if(i instanceof ReadableStream)return i;let e=i[Symbol.asyncIterator]();return new ReadableStream({async pull(t){let{done:n,value:r}=await e.next();if(n){t.close();return}t.enqueue(T(r))},cancel:()=>{e.return?.()}})}function T(i){if(i instanceof Uint8Array)return i;if(i instanceof ArrayBuffer)return new Uint8Array(i);if(ArrayBuffer.isView(i))return new Uint8Array(i.buffer,i.byteOffset,i.byteLength);if(typeof i=="string")return new TextEncoder().encode(i);throw i instanceof Blob?new Error("Pass the Blob/File to send, not blob chunks"):new Error("Unsupported stream chunk")}var S=class{#t;#e=0;#s=[];#i=!1;constructor(e){this.#t=e}async acquire(){if(this.#i)throw new Error("Transfer limiter destroyed");if(this.#e<this.#t)this.#e++;else{let t=Promise.withResolvers();if(this.#s.push(t),await t.promise,this.#i)throw this.#e--,new Error("Transfer limiter destroyed")}let e=!1;return()=>{e||(e=!0,this.#n())}}destroy(){this.#i=!0;let e=new Error("Transfer limiter destroyed");for(let t of this.#s.splice(0))t.reject(e)}#n(){if(this.#i){this.#e--;return}let e=this.#s.shift();if(e){e.resolve();return}this.#e--}};function j(i){let e=i?.maxMessageSize;return!e||!Number.isFinite(e)?65536:e}var b=class extends p{channel;#t=0;#e=null;#s=null;#i;#n=[];#r=null;#a=null;#o=!1;#l=!1;#u=Promise.resolve();#d=0;constructor(e,t,n=new S(1/0)){super(),this.channel=e,this.#i=n,t instanceof RTCPeerConnection?this.#e=t:this.#s=t,this.#f(),this.channel.binaryType="arraybuffer",this.channel.onopen=()=>this.#m(),this.channel.onclose=()=>this.destroy(),this.channel.onmessage=r=>this.#C(r.data),this.channel.onerror=r=>this.emit("error",r),this.channel.onbufferedamountlow=()=>{this.#h(),this.#p()},this.channel.readyState==="open"&&queueMicrotask(()=>this.#m())}async send(e,t={}){if(this.#l)throw new Error("Data channel closed");if(typeof e=="string"){this.#n.push(e),this.#h();return}await this.#v(async()=>{let n=await this.#i.acquire();try{await this.#b(e,t)}finally{n()}})}destroy(){if(this.#l)return;this.#l=!0,this.#n.length=0,this.#o=!1;let e=new Error("Data channel closed");this.#r?.reject(e),this.#r=null,this.#a?.error(e),this.#a=null,this.emit("close"),this.channel.onopen=null,this.channel.onclose=null,this.channel.onmessage=null,this.channel.onerror=null,this.channel.onbufferedamountlow=null,super.destroy();try{this.channel.close()}catch{}}#m(){this.#l||(this.#f(),this.#h(),this.emit("open"))}#f(){let e=j(this.#s??this.#e?.sctp??null);this.#t=Math.max(1,e-2),this.channel.bufferedAmountLowThreshold=e*2}async#v(e){let t=this.#u,{promise:n,resolve:r}=Promise.withResolvers();this.#u=n,await t;try{await e()}finally{r()}}#S(e,t){if(e instanceof Blob)return e.size-t;if(e instanceof ArrayBuffer||ArrayBuffer.isView(e))return e.byteLength-t}#w(e,t,n){n?.(e,t),this.emit("progress",{direction:"send",bytes:e,total:t})}async#b(e,t){this.#f();let n=t.offset??0;if(n!==0&&!(e instanceof Blob))throw new Error("offset requires a Blob/File");let r=e;n!==0&&e instanceof Blob&&(r=e.slice(n));let s=this.#S(e,n),a=0,c=!1;if(t.meta!==void 0){let l=new TextEncoder().encode(JSON.stringify(t.meta));this.#c(5,l),c=!0}let m=_(r),u=m.getReader(),h=!c,d=null,w=()=>{if(t.signal?.aborted)throw t.signal.reason??new Error("Aborted");if(this.#l)throw new Error("Data channel closed")},y=l=>{if(!d)return;let g=d.payload;this.#c(d.flags|(l?2:0),g),d=null,a+=g.byteLength,this.#w(a,s,t.onProgress)},o=()=>{u.cancel(t.signal?.reason??new Error("Aborted"))};t.signal?.addEventListener("abort",o,{once:!0});try{for(;;){w(),await this.#g(t.signal);let{done:l,value:g}=await u.read();if(l){d?y(!0):h?this.#c(3,new Uint8Array(0)):c&&this.#c(2,new Uint8Array(0));break}let k=T(g),C=0;for(;C<k.byteLength;){w(),await this.#g(t.signal);let x=Math.min(C+this.#t,k.byteLength);y(!1);let O=h?1:0;h=!1,d={flags:O,payload:k.subarray(C,x)},C=x}}}catch(l){try{d?y(!0):h||this.#c(2,new Uint8Array(0))}catch{}try{await u.cancel(l)}catch{}throw l}finally{t.signal?.removeEventListener("abort",o),m.locked&&u.releaseLock()}}#C(e){let t=e instanceof ArrayBuffer?B(e):null;if(!t){this.emit("message",e);return}if(this.#o){t.flags&2&&(this.#o=!1);return}if(!this.#a){if(!(t.flags&1))return;if(!this.has("stream")){this.#o=!(t.flags&2);return}let r;if(t.flags&4)try{r=JSON.parse(new TextDecoder().decode(t.payload))}catch(c){this.emit("error",c),this.#o=!(t.flags&2);return}let s,a=new ReadableStream({start:c=>{s=c,this.#a=c},cancel:()=>{this.#a=null}});if(this.#d=0,this.emit("stream",a,r),t.flags&4){t.flags&2&&(s?.close(),this.#a=null);return}}let n=this.#a;if(n){if(n.enqueue(t.payload),this.#d+=t.payload.byteLength,this.emit("progress",{direction:"receive",bytes:this.#d}),(n.desiredSize??0)<-32){n.error(new Error("Receive stream backlog exceeded")),this.#a=null,this.#o=!(t.flags&2);return}t.flags&2&&(n.close(),this.#a=null)}}#c(e,t){this.#l||(this.#n.push(M(e,t)),this.#h())}#y(){return!this.#l&&this.channel.readyState==="open"&&this.#n.length===0&&this.channel.bufferedAmount<=this.channel.bufferedAmountLowThreshold}#T(){let e=this.#n.findIndex(t=>typeof t=="string");return e>=0?this.#n.splice(e,1)[0]:this.#n.shift()}#h(){if(!(this.#l||this.channel.readyState!=="open")){for(;this.#n.length>0&&!(this.channel.bufferedAmount>this.channel.bufferedAmountLowThreshold);){let e=this.#T();if(e===void 0)break;try{typeof e=="string"?this.channel.send(e):this.channel.send(e)}catch(t){this.emit("error",t),this.#n.unshift(e);break}}this.#p()}}#g(e){if(this.#l)return Promise.reject(new Error("Data channel closed"));if(e?.aborted)return Promise.reject(e.reason??new Error("Aborted"));if(this.#y())return Promise.resolve();this.#r||(this.#r=Promise.withResolvers());let{promise:t}=this.#r,n=()=>{this.#r?.reject(e?.reason??new Error("Aborted")),this.#r=null};return e?.addEventListener("abort",n,{once:!0}),t.finally(()=>{e?.removeEventListener("abort",n)})}#p(){this.#y()&&(this.#r?.resolve(),this.#r=null)}};var E=class{remoteId;channels=new Map;get isConnected(){return this.#e.connectionState==="connected"}#t;#e;#s;#i=[];#n=!1;#r=!1;constructor(e){this.remoteId=e.remoteId,this.#t=e,this.#s=e.localId>e.remoteId,this.#e=new RTCPeerConnection(e.rtcConfig),this.#e.onicecandidate=t=>{t.candidate&&this.#t.onSignal({type:"candidate",candidate:t.candidate})},this.#e.onnegotiationneeded=async()=>{try{this.#n=!0,await this.#e.setLocalDescription(),this.#t.onSignal({type:"description",description:this.#e.localDescription})}catch{}finally{this.#n=!1}},this.#e.onconnectionstatechange=()=>{this.#e.connectionState==="connected"&&this.#t.onConnected(),["disconnected","failed","closed"].includes(this.#e.connectionState)&&this.#t.onDisconnect()},this.#e.ontrack=t=>{let n=t.streams[0];t.track.onended=()=>this.#t.onTrackEnded(t.track,n),this.#t.onTrack(t.track,n)},this.#e.ondatachannel=t=>this.#a(t.channel)}addTrack(e,t){this.#e.addTrack(e,t)}removeTrack(e){let t=this.#e.getSenders().find(n=>n.track===e);if(t)try{this.#e.removeTrack(t)}catch{}}async receiveSignal(e){try{if(e.type==="description"){let t=e.description.type==="offer"&&(this.#n||this.#e.signalingState!=="stable");if(this.#r=!this.#s&&t,this.#r)return;await this.#e.setRemoteDescription(e.description);for(let n of this.#i)await this.#e.addIceCandidate(n);this.#i=[],e.description.type==="offer"&&(await this.#e.setLocalDescription(),this.#t.onSignal({type:"description",description:this.#e.localDescription}));return}this.#e.remoteDescription?await this.#e.addIceCandidate(e.candidate):this.#i.push(e.candidate)}catch{}}createDataChannel(e,t){this.#s||this.#a(this.#e.createDataChannel(e,t))}#a(e){this.channels.get(e.label)?.destroy();let n=new b(e,this.#e,this.#t.limiter);this.channels.set(e.label,n),this.#t.onChannel(n)}destroy(){this.#e.onicecandidate=null,this.#e.onnegotiationneeded=null,this.#e.onconnectionstatechange=null,this.#e.ontrack=null,this.#e.ondatachannel=null;for(let e of this.#e.getReceivers())e.track.onended=null;for(let e of this.channels.values())e.destroy();this.channels.clear();try{this.#e.close()}catch{}}};var R=class extends p{id;rtcConfig;#t=new Map;#e;#s=new Set;#i=new Map;#n;#r=new Set;constructor({id:e,signal:t,rtcConfig:n=P,maxTransfers:r=F}){super(),this.id=e,this.#e=t,this.rtcConfig=n,this.#n=new S(r)}add(e){this.#s.add(e);for(let t of e.getTracks())for(let n of this.#t.values())n.addTrack(t,e)}remove(e){this.#s.delete(e);for(let t of e.getTracks())for(let n of this.#t.values())n.removeTrack(t)}channel(e,t={}){this.#i.set(e,t);for(let n of this.#t.values())n.createDataChannel(e,t)}getChannel(e,t){return this.#t.get(e)?.channels.get(t)}whenOpen(e,t,n={}){let r=this.getChannel(e,t);if(r?.channel.readyState==="open")return Promise.resolve(r);let{promise:s,resolve:a,reject:c}=Promise.withResolvers(),m=[],u=!1,h=(o,l)=>{if(!u){u=!0;for(let g of m)g();n.signal?.removeEventListener("abort",d),l?a(l):c(o??new Error("Channel open failed"))}},d=()=>{h(n.signal?.reason??new Error("Aborted"))};if(n.signal?.aborted)return Promise.reject(n.signal.reason??new Error("Aborted"));n.signal?.addEventListener("abort",d,{once:!0});let w=o=>{if(o.channel.readyState==="open"){h(void 0,o);return}m.push(o.on("open",()=>h(void 0,o)),o.on("close",()=>h(new Error("Data channel closed"))))},y=this.getChannel(e,t);return y&&w(y),m.push(this.on("channel",(o,l)=>{o!==e||l.channel.label!==t||w(l)}),this.on("leave",o=>{o===e&&h(new Error(`Peer left: ${e}`))})),s}connect(e){if(this.#t.has(e))return;let t=new E({localId:this.id,remoteId:e,rtcConfig:this.rtcConfig,limiter:this.#n,onSignal:n=>this.#e(e,n),onTrack:(n,r)=>this.emit("track",e,n,r),onTrackEnded:(n,r)=>this.emit("track-ended",e,n,r),onChannel:n=>{this.emit("channel",e,n);let r=()=>this.#o(e);n.on("open",r),n.channel.readyState==="open"&&r()},onConnected:()=>this.#o(e),onDisconnect:()=>{this.#t.delete(e)&&(this.#r.delete(e),t.destroy(),this.emit("leave",e))}});this.#i.forEach((n,r)=>t.createDataChannel(r,n));for(let n of this.#s)for(let r of n.getTracks())t.addTrack(r,n);this.#t.set(e,t),this.emit("join",e),this.#o(e)}receive(e,t){this.connect(e),this.#t.get(e).receiveSignal(t)}async sendTo(e,t,n,r={}){let s=this.#t.get(e);if(!s)throw new Error(`Unknown peer: ${e}`);let a=s.channels.get(t);if(!a)throw new Error(`No channel "${t}" for peer ${e}`);if(a.channel.readyState!=="open")throw new Error(`Channel "${t}" with ${e} is not open`);await a.send(n,r)}async send(e,t,n={}){let r=[];for(let s of this.#t.values()){let a=s.channels.get(e);a?.channel.readyState==="open"&&r.push(a)}if(r.length!==0){if(r.length>1&&typeof t!="string"&&!U(t))throw new Error("Pass a Blob/File/BufferSource to send to multiple peers");await Promise.all(r.map(s=>s.send(t,n)))}}close(e){let t=this.#t.get(e);t&&(this.#t.delete(e),this.#r.delete(e),t.destroy(),this.emit("leave",e))}destroy(){this.#n.destroy();for(let e of[...this.#t.keys()])this.close(e);this.#s.clear(),this.#i.clear(),this.#r.clear(),super.destroy()}#a(e){let t=this.#t.get(e);if(!t)return!1;if(this.#i.size===0)return t.isConnected;for(let n of this.#i.keys()){let r=t.channels.get(n);if(!r||r.channel.readyState!=="open")return!1}return!0}#o(e){this.#r.has(e)||this.#a(e)&&(this.#r.add(e),this.emit("ready",e))}};export{b as FastDataChannel,R as RTCManager};
1
+ var L={iceServers:[{urls:"stun:stun.l.google.com:19302"}]},B=4;var p=class{#t=new Map;on(e,t){let n=t,i=this.#t.get(e)??[];return i.push(n),this.#t.set(e,i),()=>{let a=this.#t.get(e);if(!a)return;let o=a.indexOf(n);o>=0&&a.splice(o,1),a.length===0&&this.#t.delete(e)}}has(e){return(this.#t.get(e)?.length??0)>0}emit(e,...t){let n=this.#t.get(e);if(n)for(let i of n.slice())i(...t)}destroy(){this.#t.clear()}};function A(r,e){let t=new ArrayBuffer(2+e.byteLength),n=new DataView(t);return n.setUint8(0,252),n.setUint8(1,r),new Uint8Array(t,2).set(e),t}function U(r){if(r.byteLength<2)return null;let e=new DataView(r);return e.getUint8(0)!==252?null:{flags:e.getUint8(1),payload:new Uint8Array(r,2)}}function T(){let r=Promise.resolve();return e=>{let t=r.then(e,e);return r=t.then(()=>{},()=>{}),t}}function O(r){return r instanceof Blob||r instanceof ArrayBuffer||ArrayBuffer.isView(r)}function G(r){if(r instanceof Blob)return r.stream();if(r instanceof ArrayBuffer||ArrayBuffer.isView(r)){let t=k(r);return new ReadableStream({start(n){n.enqueue(t),n.close()}})}if(r instanceof ReadableStream)return r;let e=r[Symbol.asyncIterator]();return new ReadableStream({async pull(t){let{done:n,value:i}=await e.next();if(n){t.close();return}t.enqueue(k(i))},cancel:()=>{e.return?.()}})}function k(r){if(r instanceof Uint8Array)return r;if(r instanceof ArrayBuffer)return new Uint8Array(r);if(ArrayBuffer.isView(r))return new Uint8Array(r.buffer,r.byteOffset,r.byteLength);if(typeof r=="string")return new TextEncoder().encode(r);throw r instanceof Blob?new Error("Pass the Blob/File to send, not blob chunks"):new Error("Unsupported stream chunk")}var w=class{#t;#e=0;#o=[];#n=!1;constructor(e){if(e<=0&&e!==1/0)throw new Error("max must be positive or Infinity");this.#t=e}async acquire(){if(this.#n)throw new Error("Transfer limiter destroyed");if(this.#e<this.#t)this.#e++;else{let t=Promise.withResolvers();if(this.#o.push(t),await t.promise,this.#n)throw this.#e--,new Error("Transfer limiter destroyed")}let e=!1;return()=>{e||(e=!0,this.#r())}}destroy(){this.#n=!0;let e=new Error("Transfer limiter destroyed");for(let t of this.#o.splice(0))t.reject(e)}#r(){if(this.#n){this.#e--;return}let e=this.#o.shift();if(e){e.resolve();return}this.#e--}};function K(r){let e=r?.maxMessageSize;return!e||!Number.isFinite(e)?65536:e}var C=class extends p{channel;#t=0;#e=null;#o;#n=[];#r=null;#a=null;#i=!1;#s=!1;#h=!1;#u=T();#l=0;constructor(e,t,n=new w(1/0)){super(),this.channel=e,this.#e=t,this.#o=n,this.#m(),this.channel.binaryType="arraybuffer",this.channel.onopen=()=>this.#g(),this.channel.onclose=()=>this.destroy(),this.channel.onmessage=i=>this.#T(i.data),this.channel.onerror=i=>this.emit("error",i),this.channel.onbufferedamountlow=()=>{this.#c(),this.#S()},this.channel.readyState==="open"&&queueMicrotask(()=>this.#g())}async send(e,t={}){if(this.#s)throw new Error("Data channel closed");if(typeof e=="string"){this.#p(new TextEncoder().encode(e).byteLength),this.#n.push(e),this.#c();return}await this.#u(async()=>{let n=await this.#o.acquire();try{this.#h=!0,await this.#A(e,t)}finally{this.#h=!1,n()}})}destroy(){if(this.#s)return;this.#h&&(this.#n.push(A(10,new Uint8Array(0))),this.#c()),this.#s=!0,this.#n.length=0,this.#d(new Error("Data channel closed"));let e=new Error("Data channel closed");this.#r?.reject(e),this.#r=null,this.emit("close"),this.channel.onopen=null,this.channel.onclose=null,this.channel.onmessage=null,this.channel.onerror=null,this.channel.onbufferedamountlow=null,super.destroy();try{this.channel.close()}catch{}}#g(){this.#s||(this.#m(),this.#c(),this.emit("open"))}#y(){return K(this.#e?.sctp??null)}#m(){let e=this.#y();this.#t=Math.max(1,e-2),this.channel.bufferedAmountLowThreshold=e*2}#p(e){let t=this.#y();if(e>t)throw new Error(`Message exceeds max SCTP size (${t})`)}#d(e){let t=this.#a;if(t){try{t.error(e)}catch{}this.#a=null}this.#i=!0}#b(){this.#s||this.channel.readyState!=="open"||(this.#n.push(A(10,new Uint8Array(0))),this.#c())}#C(e,t){if(e instanceof Blob)return e.size-t;if(e instanceof ArrayBuffer||ArrayBuffer.isView(e))return e.byteLength-t}#E(e,t,n){n?.(e,t),this.emit("progress",{direction:"send",bytes:e,total:t})}async#A(e,t){this.#m();let n=t.offset??0;if(n!==0&&!(e instanceof Blob))throw new Error("offset requires a Blob/File");let i=e;n!==0&&e instanceof Blob&&(i=e.slice(n));let a=this.#C(e,n),o=0,c=!1;if(t.meta!==void 0){let s=new TextEncoder().encode(JSON.stringify(t.meta));this.#f(5,s),c=!0}let g=G(i),u=g.getReader(),d=!c,f=null,v=()=>{if(t.signal?.aborted)throw t.signal.reason??new Error("Aborted");if(this.#s)throw new Error("Data channel closed")},y=s=>{if(!f)return;let h=f.payload;this.#f(f.flags|(s?2:0),h),f=null,o+=h.byteLength,this.#E(o,a,t.onProgress)},S=()=>{u.cancel(t.signal?.reason??new Error("Aborted"))};t.signal?.addEventListener("abort",S,{once:!0});try{for(;;){v(),await this.#v(t.signal);let{done:s,value:h}=await u.read();if(s){f?y(!0):d?this.#f(3,new Uint8Array(0)):c&&this.#f(2,new Uint8Array(0));break}let b=k(h),E=0;for(;E<b.byteLength;){v(),await this.#v(t.signal);let M=Math.min(E+this.#t,b.byteLength);y(!1);let q=d?1:0;d=!1,f={flags:q,payload:b.slice(E,M)},E=M}}}catch(s){try{this.#b()}catch{}try{await u.cancel(s)}catch{}throw s}finally{t.signal?.removeEventListener("abort",S),g.locked&&u.releaseLock()}}#T(e){let t=e instanceof ArrayBuffer?U(e):null;if(!t){this.emit("message",e);return}if(t.flags&8){this.#d(new Error("Stream aborted")),t.flags&2&&(this.#i=!1);return}if(this.#i){t.flags&2&&(this.#i=!1);return}if(!this.#a){if(!(t.flags&1))return;if(!this.has("stream")){this.#i=!(t.flags&2);return}let i;if(t.flags&4)try{i=JSON.parse(new TextDecoder().decode(t.payload))}catch(c){this.emit("error",c),this.#i=!(t.flags&2);return}let a,o=new ReadableStream({start:c=>{a=c,this.#a=c},cancel:()=>{this.#d(new Error("Stream cancelled"))}});if(this.#l=0,this.emit("stream",o,i),t.flags&4){t.flags&2&&(a?.close(),this.#a=null);return}}let n=this.#a;if(n){if(n.enqueue(t.payload),this.#l+=t.payload.byteLength,this.emit("progress",{direction:"receive",bytes:this.#l}),(n.desiredSize??0)<-32){if(this.#d(new Error("Receive stream backlog exceeded")),!(t.flags&2))return;this.#i=!1;return}t.flags&2&&(n.close(),this.#a=null)}}#f(e,t){this.#s||(this.#p(2+t.byteLength),this.#n.push(A(e,t)),this.#c())}#w(){return!this.#s&&this.channel.readyState==="open"&&this.#n.length===0&&this.channel.bufferedAmount<=this.channel.bufferedAmountLowThreshold}#k(){let e=this.#n.findIndex(t=>typeof t=="string");return e>=0?this.#n.splice(e,1)[0]:this.#n.shift()}#c(){if(!(this.#s||this.channel.readyState!=="open")){for(;this.#n.length>0&&!(this.channel.bufferedAmount>this.channel.bufferedAmountLowThreshold);){let e=this.#k();if(e===void 0)break;try{typeof e=="string"?this.channel.send(e):this.channel.send(e)}catch(t){this.emit("error",t),this.#n.unshift(e);break}}this.#S()}}#v(e){if(this.#s)return Promise.reject(new Error("Data channel closed"));if(e?.aborted)return Promise.reject(e.reason??new Error("Aborted"));if(this.#w())return Promise.resolve();this.#r||(this.#r=Promise.withResolvers());let{promise:t}=this.#r,n=()=>{this.#r?.reject(e?.reason??new Error("Aborted")),this.#r=null};return e?.addEventListener("abort",n,{once:!0}),t.finally(()=>{e?.removeEventListener("abort",n)})}#S(){this.#w()&&(this.#r?.resolve(),this.#r=null)}};function j(r){return r instanceof DOMException&&r.name==="InvalidStateError"}var x=class{remoteId;channels=new Map;get isConnected(){return this.#e.connectionState==="connected"}#t;#e;#o;#n=[];#r=!1;#a=!1;#i=!1;#s=T();constructor(e){this.remoteId=e.remoteId,this.#t=e,this.#o=e.localId>e.remoteId,this.#e=new RTCPeerConnection(e.rtcConfig),this.#e.onicecandidate=t=>{this.#i||!t.candidate||this.#t.onSignal({type:"candidate",candidate:t.candidate})},this.#e.onnegotiationneeded=()=>{this.#s(async()=>{if(!this.#i)try{this.#r=!0,await this.#e.setLocalDescription(),!this.#i&&this.#e.localDescription&&this.#t.onSignal({type:"description",description:this.#e.localDescription})}catch(t){j(t)||this.#t.onError(t)}finally{this.#r=!1}})},this.#e.onconnectionstatechange=()=>{this.#e.connectionState==="connected"&&this.#t.onConnected(),["disconnected","failed","closed"].includes(this.#e.connectionState)&&this.#t.onDisconnect()},this.#e.ontrack=t=>{let n=t.streams[0]??new MediaStream([t.track]);t.track.onended=()=>this.#t.onTrackEnded(t.track,n),this.#t.onTrack(t.track,n)},this.#e.ondatachannel=t=>this.#h(t.channel)}addTrack(e,t){this.#e.getSenders().some(n=>n.track===e)||this.#e.addTrack(e,t)}removeTrack(e){let t=this.#e.getSenders().find(n=>n.track===e);if(t)try{this.#e.removeTrack(t)}catch{}}receiveSignal(e){this.#s(async()=>{if(!this.#i)try{if(e.type==="description"){let t=e.description.type==="offer"&&(this.#r||this.#e.signalingState!=="stable");if(this.#a=!this.#o&&t,this.#a)return;await this.#e.setRemoteDescription(e.description);for(let n of this.#n)await this.#e.addIceCandidate(n);this.#n=[],e.description.type==="offer"&&(await this.#e.setLocalDescription(),!this.#i&&this.#e.localDescription&&this.#t.onSignal({type:"description",description:this.#e.localDescription}));return}if(this.#a)return;this.#e.remoteDescription?await this.#e.addIceCandidate(e.candidate):this.#n.push(e.candidate)}catch(t){!this.#a&&!j(t)&&this.#t.onError(t)}})}createDataChannel(e,t){this.#o||this.#h(this.#e.createDataChannel(e,t))}#h(e){let t=this.channels.get(e.label),n=new C(e,this.#e,this.#t.limiter);this.channels.set(e.label,n),t?.destroy(),this.#t.onChannel(n)}destroy(){if(!this.#i){this.#i=!0,this.#e.onicecandidate=null,this.#e.onnegotiationneeded=null,this.#e.onconnectionstatechange=null,this.#e.ontrack=null,this.#e.ondatachannel=null;for(let e of this.#e.getReceivers())e.track.onended=null;for(let e of this.channels.values())e.destroy();this.channels.clear();try{this.#e.close()}catch{}}}};var F=class extends p{id;rtcConfig;#t=new Map;#e;#o=new Set;#n=new Map;#r;#a=new Set;#i=!1;#s=new Set;constructor({id:e,signal:t,rtcConfig:n=L,maxTransfers:i=B}){super(),this.id=e,this.#e=t,this.rtcConfig=n,this.#r=new w(i)}add(e){if(this.#h(),!this.#o.has(e)){this.#o.add(e);for(let t of e.getTracks())for(let n of this.#t.values())n.addTrack(t,e)}}remove(e){this.#h(),this.#o.delete(e);for(let t of e.getTracks())for(let n of this.#t.values())n.removeTrack(t)}channel(e,t={}){this.#h(),this.#n.set(e,t);for(let n of this.#t.values())n.createDataChannel(e,t)}getChannel(e,t){return this.#t.get(e)?.channels.get(t)}whenOpen(e,t,n={}){this.#h();let i=this.getChannel(e,t);if(i?.channel.readyState==="open")return Promise.resolve(i);let{promise:a,resolve:o,reject:c}=Promise.withResolvers(),g=[],u=!1,d=(s,h)=>{if(!u){u=!0,this.#s.delete(f);for(let b of g)b();n.signal?.removeEventListener("abort",v),h?o(h):c(s??new Error("Channel open failed"))}},f=s=>{d(s)};this.#s.add(f);let v=()=>{d(n.signal?.reason??new Error("Aborted"))};if(n.signal?.aborted)return this.#s.delete(f),Promise.reject(n.signal.reason??new Error("Aborted"));n.signal?.addEventListener("abort",v,{once:!0});let y=s=>{if(s.channel.readyState==="open"){d(void 0,s);return}g.push(s.on("open",()=>d(void 0,s)),s.on("close",()=>{let h=this.getChannel(e,t);if(h&&h!==s){y(h);return}d(new Error("Data channel closed"))}))},S=this.getChannel(e,t);return S&&y(S),g.push(this.on("channel",(s,h)=>{s!==e||h.channel.label!==t||y(h)}),this.on("leave",s=>{s===e&&d(new Error(`Peer left: ${e}`))})),a}connect(e){if(this.#h(),e===this.id)throw new Error("Cannot connect to self");if(this.#t.has(e))return;let t=new x({localId:this.id,remoteId:e,rtcConfig:this.rtcConfig,limiter:this.#r,onSignal:n=>this.#e(e,n),onError:n=>this.emit("error",e,n),onTrack:(n,i)=>this.emit("track",e,n,i),onTrackEnded:(n,i)=>this.emit("track-ended",e,n,i),onChannel:n=>{this.emit("channel",e,n);let i=()=>this.#l(e);n.on("open",i),n.channel.readyState==="open"&&i()},onConnected:()=>this.#l(e),onDisconnect:()=>{this.#t.delete(e)&&(this.#a.delete(e),t.destroy(),this.emit("leave",e))}});this.#n.forEach((n,i)=>t.createDataChannel(i,n));for(let n of this.#o)for(let i of n.getTracks())t.addTrack(i,n);this.#t.set(e,t),this.emit("join",e),this.#l(e)}receive(e,t){this.#h(),this.connect(e),this.#t.get(e).receiveSignal(t)}async sendTo(e,t,n,i={}){this.#h();let a=this.#t.get(e);if(!a)throw new Error(`Unknown peer: ${e}`);let o=a.channels.get(t);if(!o)throw new Error(`No channel "${t}" for peer ${e}`);if(o.channel.readyState!=="open")throw new Error(`Channel "${t}" with ${e} is not open`);await o.send(n,i)}async send(e,t,n={}){this.#h();let i=[];for(let a of this.#t.values()){let o=a.channels.get(e);o?.channel.readyState==="open"&&i.push(o)}if(i.length!==0){if(i.length>1&&typeof t!="string"&&!O(t))throw new Error("Pass a Blob/File/BufferSource to send to multiple peers");await Promise.all(i.map(a=>a.send(t,n)))}}close(e){let t=this.#t.get(e);t&&(this.#t.delete(e),this.#a.delete(e),t.destroy(),this.emit("leave",e))}destroy(){if(this.#i)return;this.#i=!0;let e=new Error("RTCManager destroyed");for(let t of this.#s)t(e);this.#s.clear(),this.#r.destroy();for(let t of[...this.#t.keys()])this.close(t);this.#o.clear(),this.#n.clear(),this.#a.clear(),super.destroy()}#h(){if(this.#i)throw new Error("RTCManager destroyed")}#u(e){let t=this.#t.get(e);if(!t)return!1;if(this.#n.size===0)return t.isConnected;for(let n of this.#n.keys()){let i=t.channels.get(n);if(!i||i.channel.readyState!=="open")return!1}return!0}#l(e){this.#a.has(e)||this.#u(e)&&(this.#a.add(e),this.emit("ready",e))}};export{C as FastDataChannel,F as RTCManager};
@@ -7,6 +7,7 @@ type PeerNodeOptions = {
7
7
  rtcConfig: RTCConfiguration;
8
8
  limiter: TransferLimiter;
9
9
  onSignal: (payload: SignalPayload) => void;
10
+ onError: (error: unknown) => void;
10
11
  onTrack: (track: MediaStreamTrack, stream: MediaStream) => void;
11
12
  onTrackEnded: (track: MediaStreamTrack, stream: MediaStream) => void;
12
13
  onChannel: (channel: FastDataChannel) => void;
@@ -21,7 +22,7 @@ export declare class PeerNode {
21
22
  constructor(opts: PeerNodeOptions);
22
23
  addTrack(track: MediaStreamTrack, stream: MediaStream): void;
23
24
  removeTrack(track: MediaStreamTrack): void;
24
- receiveSignal(signal: SignalPayload): Promise<void>;
25
+ receiveSignal(signal: SignalPayload): void;
25
26
  createDataChannel(label: string, options?: RTCDataChannelInit): void;
26
27
  destroy(): void;
27
28
  }
@@ -0,0 +1 @@
1
+ export declare function createQueue(): (job: () => Promise<void>) => Promise<void>;
@@ -21,6 +21,7 @@ interface RTCManagerEvents {
21
21
  channel: (id: string, channel: FastDataChannel) => void;
22
22
  track: (id: string, track: MediaStreamTrack, stream: MediaStream) => void;
23
23
  "track-ended": (id: string, track: MediaStreamTrack, stream: MediaStream) => void;
24
+ error: (id: string, error: unknown) => void;
24
25
  }
25
26
  export declare class RTCManager extends EventEmitter<RTCManagerEvents> {
26
27
  #private;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastrtc",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Easy to use, zero-dependency WebRTC mesh library for browsers",
5
5
  "keywords": [
6
6
  "datachannel",
@@ -21,17 +21,20 @@
21
21
  "types": "dist/index.d.ts",
22
22
  "exports": {
23
23
  ".": {
24
+ "types": "./dist/index.d.ts",
24
25
  "import": "./dist/index.js",
25
26
  "require": "./dist/index.cjs"
26
27
  }
27
28
  },
28
29
  "devDependencies": {
29
30
  "typescript": "^7.0.2",
30
- "tsup": "^8.5.1"
31
+ "tsup": "^8.5.1",
32
+ "vitest": "^3.2.4"
31
33
  },
32
34
  "scripts": {
33
35
  "clean": "rm -rf dist",
34
36
  "build": "pnpm clean && tsup && tsc",
35
- "test": "echo \"Error: no test specified\" && exit 1"
37
+ "test": "vitest run",
38
+ "test:watch": "vitest"
36
39
  }
37
40
  }