fastrtc 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # fastrtc
2
2
 
3
- Browser WebRTC mesh library. Zero runtime dependencies.
3
+ Browser WebRTC mesh helper. Zero runtime dependencies.
4
4
 
5
- You wire signaling (WebSocket, SSE, whatever). fastrtc handles peer connections, data channels, and media tracks.
5
+ You wire signaling. fastrtc connects peers and opens labeled data channels. Each channel tracks SCTP `maxMessageSize` and applies send backpressure.
6
6
 
7
7
  ## Mental model
8
8
 
@@ -12,34 +12,29 @@ You fastrtc Remote peer
12
12
  │ signal(to, payload) ───────►│ forwards SDP / ICE │
13
13
  │◄──── receive(from, payload) ──│ │
14
14
  │ │◄──────── WebRTC ────────────────►│
15
- │ │ │
16
15
  │ channel("chat") │ opens labeled data channels │
17
16
  │ connect("bob") │ negotiates connection │
18
- on("ready") all channels open → safe to send
19
- │ send / sendTo text or streamed files
17
+ peer.ready declared channels open
18
+ │ send / sendTo / ch.send string or BufferSource
20
19
  ```
21
20
 
22
- **Three things to remember:**
23
-
24
- 1. **Signaling is yours** — forward `SignalPayload` objects between peers. fastrtc never touches your server.
25
- 2. **Declare channels before `connect()`** — `channel("files")` registers a label for every current and future peer.
26
- 3. **`ready` means go** — wait for `ready` (or `whenOpen`) before sending on a channel.
21
+ 1. **Signaling is yours** — forward `SignalPayload` objects. fastrtc never touches your server.
22
+ 2. **Declare channels before `connect()`** — `channel("chat")` registers a label for every current and future peer.
23
+ 3. **`peer.ready` means go** — wait for it (or `peer.open(label)`) before sending.
27
24
 
28
25
  ## Install
29
26
 
30
27
  ```bash
31
28
  pnpm add fastrtc
32
- # or
33
- npm install fastrtc
34
29
  ```
35
30
 
36
31
  ## Quick start
37
32
 
38
33
  ```ts
39
- import { RTCManager } from "fastrtc";
34
+ import { FastRTC } from "fastrtc";
40
35
  import type { SignalPayload } from "fastrtc";
41
36
 
42
- const rtc = new RTCManager({
37
+ const rtc = new FastRTC({
43
38
  id: "alice",
44
39
  signal: (to, payload) => {
45
40
  ws.send(JSON.stringify({ to, from: "alice", payload }));
@@ -52,18 +47,24 @@ ws.onmessage = (e) => {
52
47
  };
53
48
 
54
49
  rtc.channel("chat", { ordered: true });
55
- rtc.connect("bob");
56
50
 
57
- rtc.on("channel", (_id, ch) => {
58
- ch.on("message", (data) => console.log(data));
59
- ch.on("open", () => void ch.send("hello"));
51
+ rtc.addEventListener("join", ({ peer }) => {
52
+ void peer.ready.then(() => {
53
+ void peer.channels.get("chat")!.send("hello");
54
+ });
60
55
  });
56
+
57
+ const bob = rtc.connect("bob");
58
+ await bob.ready;
59
+ const chat = bob.channels.get("chat")!;
60
+ chat.addEventListener("message", ({ data }) => console.log(data));
61
+ await chat.send("hi");
62
+ await rtc.send("chat", "hello all");
63
+ await rtc.sendTo("bob", "chat", "hello bob");
61
64
  ```
62
65
 
63
66
  ## Signaling
64
67
 
65
- Each outbound signal is a `SignalPayload`:
66
-
67
68
  ```ts
68
69
  type SignalPayload =
69
70
  | { type: "description"; description: RTCSessionDescriptionInit }
@@ -71,157 +72,112 @@ type SignalPayload =
71
72
  ```
72
73
 
73
74
  - `signal(to, payload)` — fastrtc needs to send SDP or ICE to `to`
74
- - `receive(from, payload)` — apply a remote signal (auto-connects if needed)
75
+ - `receive(from, payload)` — apply a remote signal (auto-connects, returns `Peer`)
75
76
 
76
77
  **Offer glare:** peer with the lexicographically larger ID is *polite* and yields on collision. Only the *impolite* peer creates labeled data channels; the polite peer waits for `ondatachannel`.
77
78
 
78
- ## Data channels
79
-
80
- ### Setup
79
+ ## Channels
81
80
 
82
81
  ```ts
83
82
  rtc.channel("chat", { ordered: true });
84
- rtc.channel("files", { ordered: true });
85
-
86
- rtc.on("channel", (peerId, ch) => {
87
- ch.on("open", () => { /* channel is open for this peer */ });
88
- ch.on("message", (data) => { /* string message */ });
89
- ch.on("stream", async (stream, meta) => {
90
- const blob = await new Response(stream).blob();
91
- console.log(peerId, meta, blob.size);
92
- });
93
- ch.on("progress", ({ direction, bytes, total }) => {
94
- console.log(direction, bytes, total);
95
- });
96
- });
83
+ rtc.channel("files");
97
84
 
98
- rtc.on("ready", (peerId) => {
99
- // every declared channel is open for this peer
100
- void rtc.sendTo(peerId, "files", file);
101
- });
85
+ const bob = rtc.connect("bob");
86
+ await bob.ready;
87
+ const chat = bob.channels.get("chat")!;
88
+ await chat.send("hello");
102
89
  ```
103
90
 
104
- | Event | When |
105
- | ------- | ---- |
106
- | `join` | Peer connection created |
107
- | `ready` | All declared channels open for that peer (fires once) |
108
- | `leave` | Peer disconnected (including ICE `disconnected`) |
109
- | `error` | Signaling or negotiation failed for a peer |
91
+ `Channel.send` waits until `bufferedAmount` is under the watermark (`maxMessageSize * 2`), then hands the payload to SCTP. Parallel sends on the same channel queue. Payload larger than `channel.maxMessageSize` throws — caller splits. `maxMessageSize` comes from `pc.sctp.maxMessageSize` after the channel opens (fallback `65536`).
110
92
 
111
- Get a channel directly: `rtc.getChannel(peerId, "files")` or `await rtc.whenOpen(peerId, "files")`.
93
+ `string | BufferSource` only. No framing, no Blob/stream helpers.
112
94
 
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.
95
+ Late labels after `ready`: `await bob.open("files")`.
114
96
 
115
- ### Sending
97
+ ## Broadcast
116
98
 
117
99
  | Method | Target |
118
100
  | ------ | ------ |
119
- | `send(label, data)` | Every peer with an open channel on `label` |
101
+ | `send(label, data)` | Every peer with that label open |
120
102
  | `sendTo(peerId, label, data)` | One peer |
121
- | `ch.send(data)` | When you already have the `FastDataChannel` |
122
-
123
- **Data types:**
124
-
125
- - **String** → single SCTP message → `message` event
126
- - **Blob / File / BufferSource / ReadableStream / async iterable** → framed stream → `stream` event
103
+ | `channel.send(data)` | Channel you already have |
127
104
 
128
- ```ts
129
- await rtc.send("files", file); // all peers
130
- await rtc.sendTo("bob", "files", file); // one peer
131
- await rtc.sendTo("bob", "files", file, {
132
- meta: { name: file.name }, // JSON on first frame
133
- offset: 1024, // Blob/File slice start
134
- onProgress: (bytes, total) => { /* ... */ },
135
- signal: abortController.signal,
136
- });
137
- ```
105
+ No open targets on `send` → resolves, does not throw. Both honor `{ signal?: AbortSignal }`.
138
106
 
139
- **Multi-peer file sends:** pass the `Blob`/`File`, not `blob.stream()`. Each peer gets its own `blob.stream()`, so a slow peer does not block others. A late joiner gets a fresh stream from byte 0.
107
+ ## Events
140
108
 
141
- **ReadableStream caveat:** one-shot streams work with `sendTo` only. `send()` to multiple peers throws — use a `Blob`/`File` instead.
109
+ `FastRTC` is an `EventTarget`:
142
110
 
143
- Strings are drained before queued file frames, so control messages are not stuck behind a large send.
111
+ | Event | Payload |
112
+ | ----- | ------- |
113
+ | `join` | `peer` |
114
+ | `leave` | `id` |
115
+ | `error` | `id`, `error` |
144
116
 
145
- ### Receiving
117
+ `Channel` events: `message` (`data`), `close`, `error`.
146
118
 
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).
119
+ ICE `disconnected` does not tear down (can recover). `failed` / `closed` emit `leave`.
148
120
 
149
121
  ## Media
150
122
 
151
- ```ts
152
- const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
153
- rtc.add(stream); // tracks on the stream at add-time attach to current and future peers
154
-
155
- rtc.on("track", (peerId, track, stream) => {
156
- videoElement.srcObject = stream;
157
- });
158
-
159
- rtc.on("track-ended", (peerId, track) => { /* remote track stopped */ });
160
-
161
- rtc.remove(stream);
162
- ```
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
-
166
- ## Transfer limiter
167
-
168
- Large file sends use memory. `maxTransfers` caps how many run at once (default `4`); extras wait in FIFO.
123
+ No media helpers. Use the raw connection:
169
124
 
170
125
  ```ts
171
- const rtc = new RTCManager({ id: "alice", signal, maxTransfers: 4 });
172
- // Infinity = unlimited
126
+ bob.connection.addTrack(track, stream);
127
+ bob.connection.ontrack = (e) => {
128
+ video.srcObject = e.streams[0];
129
+ };
173
130
  ```
174
131
 
175
- Each outgoing framed send holds one slot until complete. Same-channel sends are serialized and do not hold a slot while waiting.
176
-
177
132
  ## API
178
133
 
179
- ### `RTCManager`
134
+ ### `FastRTC`
180
135
 
181
136
  ```ts
182
- new RTCManager({
137
+ new FastRTC({
183
138
  id: string;
184
139
  signal: (to: string, payload: SignalPayload) => void;
185
- rtcConfig?: RTCConfiguration; // default: Google STUN
186
- maxTransfers?: number; // default: 4
140
+ rtcConfig?: RTCConfiguration; // default: Google STUN
187
141
  })
188
142
  ```
189
143
 
190
144
  | Method | Description |
191
145
  | ------ | ----------- |
192
- | `connect(remoteId)` | Open connection to a peer |
193
- | `receive(remoteId, payload)` | Apply remote SDP/ICE |
194
146
  | `channel(label, options?)` | Register a labeled channel for all peers |
195
- | `getChannel(peerId, label)` | Existing channel, or `undefined` |
196
- | `whenOpen(peerId, label, { signal? })` | Promise for an open channel |
197
- | `send(label, data, options?)` | Send to every open peer on `label` |
147
+ | `connect(id, { signal? })` | Open connection; idempotent; returns `Peer` |
148
+ | `receive(id, payload)` | Apply remote SDP/ICE; auto-connects; returns `Peer` |
149
+ | `get(id)` | `Peer` or `undefined` |
150
+ | `send(label, data, options?)` | Broadcast to every open channel on `label` |
198
151
  | `sendTo(peerId, label, data, options?)` | Send to one peer |
199
- | `add(stream)` / `remove(stream)` | Manage outgoing media tracks |
200
- | `close(remoteId)` | Tear down one peer |
201
- | `destroy()` | Close everything, reject queued transfers |
152
+ | `close(id)` | Tear down one peer |
153
+ | `dispose()` / `[Symbol.dispose]()` | Close everything |
202
154
 
203
155
  | Property | Description |
204
156
  | -------- | ----------- |
205
157
  | `id` | Local peer ID |
206
158
  | `rtcConfig` | Active RTC configuration |
159
+ | `peers` | `ReadonlyMap<string, Peer>` |
207
160
 
208
- **Events:** `join`, `ready`, `leave`, `channel`, `track`, `track-ended`, `error`. `.on(event, handler)` returns unsubscribe.
209
-
210
- ### `FastDataChannel`
211
-
212
- Wraps `RTCDataChannel` with streaming, backpressure, and typed events.
161
+ ### `Peer`
213
162
 
214
163
  | Method / property | Description |
215
164
  | ----------------- | ----------- |
216
- | `channel` | Underlying `RTCDataChannel` |
217
- | `send(data, options?)` | String message; else → framed stream |
218
- | `destroy()` | Close and detach |
165
+ | `id` | Remote peer ID |
166
+ | `connection` | Underlying `RTCPeerConnection` |
167
+ | `channels` | `ReadonlyMap<string, Channel>` |
168
+ | `ready` | Resolves when every declared label is open (or ICE connected if none) |
169
+ | `open(label, { signal? })` | Promise for one open channel |
170
+ | `close()` | Tear down this peer |
219
171
 
220
- **Events:** `open`, `close`, `message`, `error`, `stream`, `progress`.
172
+ ### `Channel`
221
173
 
222
- ### Exports
223
-
224
- `RTCManager`, `FastDataChannel`, `SignalPayload`, `SendData`, `SendOptions`
174
+ | Method / property | Description |
175
+ | ----------------- | ----------- |
176
+ | `label` / `readyState` / `maxMessageSize` | Channel state |
177
+ | `raw` | Underlying `RTCDataChannel` |
178
+ | `ready` | Resolves when open and SCTP size applied |
179
+ | `send(data, { signal? })` | Backpressured send; throws if oversized |
180
+ | `close()` / `[Symbol.dispose]()` | Close the channel |
225
181
 
226
182
  ## Build
227
183
 
@@ -0,0 +1,29 @@
1
+ export type SendOptions = {
2
+ signal?: AbortSignal;
3
+ };
4
+ export declare class ChannelErrorEvent extends Event {
5
+ readonly error: unknown;
6
+ constructor(error: unknown);
7
+ }
8
+ interface ChannelEventMap {
9
+ message: MessageEvent;
10
+ close: Event;
11
+ error: ChannelErrorEvent;
12
+ }
13
+ export declare class Channel extends EventTarget {
14
+ #private;
15
+ readonly raw: RTCDataChannel;
16
+ readonly ready: Promise<Channel>;
17
+ constructor(channel: RTCDataChannel, pc: RTCPeerConnection);
18
+ get label(): string;
19
+ get readyState(): RTCDataChannelState;
20
+ get maxMessageSize(): number;
21
+ send(data: string | BufferSource, options?: SendOptions): Promise<void>;
22
+ close(): void;
23
+ [Symbol.dispose](): void;
24
+ addEventListener<K extends keyof ChannelEventMap>(type: K, listener: (this: Channel, ev: ChannelEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void;
25
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void;
26
+ removeEventListener<K extends keyof ChannelEventMap>(type: K, listener: (this: Channel, ev: ChannelEventMap[K]) => void, options?: boolean | EventListenerOptions): void;
27
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void;
28
+ }
29
+ export {};
package/dist/consts.d.ts CHANGED
@@ -1,10 +1,2 @@
1
- export declare const FRAME_MAGIC = 252;
2
- export declare const FLAG_FIRST = 1;
3
- export declare const FLAG_LAST = 2;
4
- export declare const FLAG_META = 4;
5
- export declare const FLAG_ABORT = 8;
6
- export declare const FRAME_HEADER_SIZE = 2;
7
- export declare const RECEIVE_BACKLOG = 32;
8
1
  export declare const SCTP_DEFAULT_MESSAGE_SIZE = 65536;
9
2
  export declare const DEFAULT_RTC_CONFIG: RTCConfiguration;
10
- export declare const DEFAULT_MAX_TRANSFERS = 4;
package/dist/index.cjs CHANGED
@@ -1 +1 @@
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});
1
+ "use strict";var C=Object.defineProperty;var P=Object.getOwnPropertyDescriptor;var O=Object.getOwnPropertyNames;var T=Object.prototype.hasOwnProperty;var R=(i,e)=>{for(var t in e)C(i,t,{get:e[t],enumerable:!0})},x=(i,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of O(e))!T.call(i,r)&&r!==t&&C(i,r,{get:()=>e[r],enumerable:!(n=P(e,r))||n.enumerable});return i};var D=i=>x(C({},"__esModule",{value:!0}),i);var k={};R(k,{Channel:()=>a,ChannelErrorEvent:()=>l,ErrorEvent:()=>v,FastRTC:()=>E,JoinEvent:()=>d,LeaveEvent:()=>f,Peer:()=>c});module.exports=D(k);var m={iceServers:[{urls:"stun:stun.l.google.com:19302"}]};function g(){let i=Promise.resolve();return e=>{let t=i.then(e,e);return i=t.then(()=>{},()=>{}),t}}var l=class extends Event{constructor(t){super("error");this.error=t}error};function A(i){return typeof i=="string"?new TextEncoder().encode(i).byteLength:i.byteLength}function M(i){let e=i?.maxMessageSize;return!e||!Number.isFinite(e)?65536:e}var a=class extends EventTarget{raw;ready;#e;#r=65536;#t=!1;#s=!1;#o=Promise.withResolvers();#n=null;#c=g();constructor(e,t){super(),this.raw=e,this.#e=t,this.ready=this.#o.promise,this.ready.catch(()=>{}),this.raw.binaryType="arraybuffer",this.#a(),this.raw.onopen=()=>this.#i(),this.raw.onclose=()=>this.close(),this.raw.onmessage=n=>{this.dispatchEvent(new MessageEvent("message",{data:n.data}))},this.raw.onerror=n=>{this.dispatchEvent(new l(n))},this.raw.onbufferedamountlow=()=>this.#d(),this.raw.readyState==="open"&&queueMicrotask(()=>this.#i())}get label(){return this.raw.label}get readyState(){return this.raw.readyState}get maxMessageSize(){return this.#r}async send(e,t={}){if(this.#t||this.raw.readyState!=="open")throw new Error("Data channel closed");if(A(e)>this.#r)throw new Error(`Message exceeds max SCTP size (${this.#r})`);if(t.signal?.aborted)throw t.signal.reason??new Error("Aborted");await this.#c(async()=>{if(this.#t||this.raw.readyState!=="open")throw new Error("Data channel closed");if(t.signal?.aborted)throw t.signal.reason??new Error("Aborted");if(await this.#l(t.signal),this.#t||this.raw.readyState!=="open")throw new Error("Data channel closed");if(t.signal?.aborted)throw t.signal.reason??new Error("Aborted");if(typeof e=="string"){this.raw.send(e);return}if(e instanceof ArrayBuffer){this.raw.send(e);return}this.raw.send(e)})}close(){if(!this.#t){this.#t=!0,this.#n?.reject(new Error("Data channel closed")),this.#n=null,this.#s||(this.#s=!0,this.#o.reject(new Error("Data channel closed"))),this.dispatchEvent(new Event("close")),this.raw.onopen=null,this.raw.onclose=null,this.raw.onmessage=null,this.raw.onerror=null,this.raw.onbufferedamountlow=null;try{this.raw.close()}catch{}}}[Symbol.dispose](){this.close()}addEventListener(e,t,n){super.addEventListener(e,t,n)}removeEventListener(e,t,n){super.removeEventListener(e,t,n)}#i(){this.#t||this.#s||(this.#s=!0,this.#a(),this.#d(),this.#o.resolve(this))}#a(){this.#r=M(this.#e.sctp??null),this.raw.bufferedAmountLowThreshold=this.#r*2}#h(){return!this.#t&&this.raw.readyState==="open"&&this.raw.bufferedAmount<=this.raw.bufferedAmountLowThreshold}#l(e){if(this.#t)return Promise.reject(new Error("Data channel closed"));if(e?.aborted)return Promise.reject(e.reason??new Error("Aborted"));if(this.#h())return Promise.resolve();this.#n||(this.#n=Promise.withResolvers());let{promise:t}=this.#n,n=()=>{this.#n?.reject(e?.reason??new Error("Aborted")),this.#n=null};return e?.addEventListener("abort",n,{once:!0}),t.finally(()=>{e?.removeEventListener("abort",n)})}#d(){this.#h()&&(this.#n?.resolve(),this.#n=null)}};function L(i){return i instanceof DOMException&&i.name==="InvalidStateError"}var c=class{id;ready;connection;#e=new Map;#r=new Set;#t;#s;#o=[];#n=!1;#c=!1;#i=!1;#a=!1;#h=Promise.withResolvers();#l=new Set;#d=g();constructor(e){this.id=e.remoteId,this.#t=e,this.#s=e.localId>e.remoteId,this.connection=new RTCPeerConnection(e.rtcConfig),this.ready=this.#h.promise,this.ready.catch(()=>{}),this.connection.onicecandidate=t=>{this.#i||!t.candidate||this.#t.onSignal({type:"candidate",candidate:t.candidate})},this.connection.onnegotiationneeded=()=>{this.#d(async()=>{if(!this.#i)try{this.#n=!0,await this.connection.setLocalDescription(),!this.#i&&this.connection.localDescription&&this.#t.onSignal({type:"description",description:this.connection.localDescription})}catch(t){L(t)||this.#t.onError(t)}finally{this.#n=!1}})},this.connection.onconnectionstatechange=()=>{if(this.connection.connectionState==="connected"&&this.#f(),this.connection.connectionState==="failed"||this.connection.connectionState==="closed"){if(this.#i)return;this.#t.onDisconnect()}},this.connection.ondatachannel=t=>this.#p(t.channel)}get channels(){return this.#e}createDataChannel(e,t){this.#r.add(e),!this.#s&&!this.#e.has(e)&&this.#p(this.connection.createDataChannel(e,t)),this.#f()}receiveSignal(e){this.#d(async()=>{if(!this.#i)try{if(e.type==="description"){let t=e.description.type==="offer"&&(this.#n||this.connection.signalingState!=="stable");if(this.#c=!this.#s&&t,this.#c)return;await this.connection.setRemoteDescription(e.description);for(let n of this.#o)await this.connection.addIceCandidate(n);this.#o=[],e.description.type==="offer"&&(await this.connection.setLocalDescription(),!this.#i&&this.connection.localDescription&&this.#t.onSignal({type:"description",description:this.connection.localDescription}));return}if(this.#c)return;this.connection.remoteDescription?await this.connection.addIceCandidate(e.candidate):this.#o.push(e.candidate)}catch(t){!this.#c&&!L(t)&&this.#t.onError(t)}})}open(e,t={}){if(this.#i)return Promise.reject(new Error("Peer closed"));if(t.signal?.aborted)return Promise.reject(t.signal.reason??new Error("Aborted"));let n=this.#e.get(e);if(n?.readyState==="open")return Promise.resolve(n);let{promise:r,resolve:o,reject:s}=Promise.withResolvers(),p=!1,w=(u,h)=>{p||(p=!0,this.#l.delete(y),t.signal?.removeEventListener("abort",S),h?o(h):s(u??new Error("Channel open failed")))},y=u=>{if(u){w(u);return}let h=this.#e.get(e);h?.readyState==="open"&&w(void 0,h)},S=()=>{w(t.signal?.reason??new Error("Aborted"))};return t.signal?.addEventListener("abort",S,{once:!0}),this.#l.add(y),y(),r}close(){if(!this.#i){this.#i=!0,this.#g(new Error("Peer closed")),this.connection.onicecandidate=null,this.connection.onnegotiationneeded=null,this.connection.onconnectionstatechange=null,this.connection.ondatachannel=null;for(let e of this.#e.values())e.close();this.#e.clear();try{this.connection.close()}catch{}}}#p(e){let t=this.#e.get(e.label),n=new a(e,this.connection);this.#e.set(e.label,n),t?.close(),n.ready.then(()=>{this.#v(),this.#f()},()=>this.#v()),this.#v()}#u(){if(this.#r.size===0)return this.connection.connectionState==="connected";for(let e of this.#r){let t=this.#e.get(e);if(!t||t.readyState!=="open")return!1}return!0}#f(){this.#a||this.#i||this.#u()&&(this.#a=!0,this.#h.resolve(this))}#v(){let e=this.#i?new Error("Peer closed"):void 0;for(let t of[...this.#l])t(e)}#g(e){this.#a||(this.#a=!0,this.#h.reject(e));for(let t of[...this.#l])t(e)}};var d=class extends Event{constructor(t){super("join");this.peer=t}peer},f=class extends Event{constructor(t){super("leave");this.id=t}id},v=class extends Event{constructor(t,n){super("error");this.id=t;this.error=n}id;error},E=class extends EventTarget{id;rtcConfig;#e=new Map;#r;#t=new Map;#s=new Map;#o=!1;constructor({id:e,signal:t,rtcConfig:n=m}){super(),this.id=e,this.#r=t,this.rtcConfig=n}get peers(){return this.#e}get(e){return this.#e.get(e)}channel(e,t={}){this.#n(),this.#t.set(e,t);for(let n of this.#e.values())n.createDataChannel(e,t)}connect(e,t={}){if(this.#n(),e===this.id)throw new Error("Cannot connect to self");let n=this.#e.get(e);if(n)return n;if(t.signal?.aborted)throw t.signal.reason??new Error("Aborted");let r=new c({localId:this.id,remoteId:e,rtcConfig:this.rtcConfig,onSignal:s=>this.#r(e,s),onError:s=>this.dispatchEvent(new v(e,s)),onDisconnect:()=>this.close(e)});this.#t.forEach((s,p)=>{r.createDataChannel(p,s)}),this.#e.set(e,r);let o=t.signal;if(o){let s=()=>this.close(e);o.addEventListener("abort",s),this.#s.set(e,()=>o.removeEventListener("abort",s))}return this.dispatchEvent(new d(r)),r}receive(e,t){this.#n();let n=this.connect(e);return n.receiveSignal(t),n}async send(e,t,n={}){this.#n();let r=[];for(let o of this.#e.values()){let s=o.channels.get(e);s?.readyState==="open"&&r.push(s.send(t,n))}r.length!==0&&await Promise.all(r)}async sendTo(e,t,n,r={}){this.#n();let o=this.#e.get(e);if(!o)throw new Error(`Unknown peer: ${e}`);let s=o.channels.get(t);if(!s)throw new Error(`No channel "${t}" for peer ${e}`);if(s.readyState!=="open")throw new Error(`Channel "${t}" with ${e} is not open`);await s.send(n,r)}close(e){let t=this.#e.get(e);t&&(this.#e.delete(e),this.#s.get(e)?.(),this.#s.delete(e),t.close(),this.dispatchEvent(new f(e)))}dispose(){if(!this.#o){this.#o=!0;for(let e of[...this.#e.keys()])this.close(e);this.#t.clear()}}[Symbol.dispose](){this.dispose()}addEventListener(e,t,n){super.addEventListener(e,t,n)}removeEventListener(e,t,n){super.removeEventListener(e,t,n)}#n(){if(this.#o)throw new Error("FastRTC disposed")}};0&&(module.exports={Channel,ChannelErrorEvent,ErrorEvent,FastRTC,JoinEvent,LeaveEvent,Peer});
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { FastDataChannel } from "./fast-data-channel";
2
- export { RTCManager, type SignalPayload } from "./rtc-manager";
3
- export type { SendData, SendOptions } from "./stream-source";
1
+ export { Channel, ChannelErrorEvent, type SendOptions } from "./channel";
2
+ export { Peer, type SignalPayload } from "./peer";
3
+ export { ErrorEvent, FastRTC, JoinEvent, LeaveEvent } from "./rtc";
package/dist/index.js CHANGED
@@ -1 +1 @@
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};
1
+ var S={iceServers:[{urls:"stun:stun.l.google.com:19302"}]};function f(){let i=Promise.resolve();return e=>{let t=i.then(e,e);return i=t.then(()=>{},()=>{}),t}}var v=class extends Event{constructor(t){super("error");this.error=t}error};function L(i){return typeof i=="string"?new TextEncoder().encode(i).byteLength:i.byteLength}function P(i){let e=i?.maxMessageSize;return!e||!Number.isFinite(e)?65536:e}var c=class extends EventTarget{raw;ready;#e;#r=65536;#t=!1;#s=!1;#o=Promise.withResolvers();#n=null;#c=f();constructor(e,t){super(),this.raw=e,this.#e=t,this.ready=this.#o.promise,this.ready.catch(()=>{}),this.raw.binaryType="arraybuffer",this.#a(),this.raw.onopen=()=>this.#i(),this.raw.onclose=()=>this.close(),this.raw.onmessage=n=>{this.dispatchEvent(new MessageEvent("message",{data:n.data}))},this.raw.onerror=n=>{this.dispatchEvent(new v(n))},this.raw.onbufferedamountlow=()=>this.#d(),this.raw.readyState==="open"&&queueMicrotask(()=>this.#i())}get label(){return this.raw.label}get readyState(){return this.raw.readyState}get maxMessageSize(){return this.#r}async send(e,t={}){if(this.#t||this.raw.readyState!=="open")throw new Error("Data channel closed");if(L(e)>this.#r)throw new Error(`Message exceeds max SCTP size (${this.#r})`);if(t.signal?.aborted)throw t.signal.reason??new Error("Aborted");await this.#c(async()=>{if(this.#t||this.raw.readyState!=="open")throw new Error("Data channel closed");if(t.signal?.aborted)throw t.signal.reason??new Error("Aborted");if(await this.#l(t.signal),this.#t||this.raw.readyState!=="open")throw new Error("Data channel closed");if(t.signal?.aborted)throw t.signal.reason??new Error("Aborted");if(typeof e=="string"){this.raw.send(e);return}if(e instanceof ArrayBuffer){this.raw.send(e);return}this.raw.send(e)})}close(){if(!this.#t){this.#t=!0,this.#n?.reject(new Error("Data channel closed")),this.#n=null,this.#s||(this.#s=!0,this.#o.reject(new Error("Data channel closed"))),this.dispatchEvent(new Event("close")),this.raw.onopen=null,this.raw.onclose=null,this.raw.onmessage=null,this.raw.onerror=null,this.raw.onbufferedamountlow=null;try{this.raw.close()}catch{}}}[Symbol.dispose](){this.close()}addEventListener(e,t,n){super.addEventListener(e,t,n)}removeEventListener(e,t,n){super.removeEventListener(e,t,n)}#i(){this.#t||this.#s||(this.#s=!0,this.#a(),this.#d(),this.#o.resolve(this))}#a(){this.#r=P(this.#e.sctp??null),this.raw.bufferedAmountLowThreshold=this.#r*2}#h(){return!this.#t&&this.raw.readyState==="open"&&this.raw.bufferedAmount<=this.raw.bufferedAmountLowThreshold}#l(e){if(this.#t)return Promise.reject(new Error("Data channel closed"));if(e?.aborted)return Promise.reject(e.reason??new Error("Aborted"));if(this.#h())return Promise.resolve();this.#n||(this.#n=Promise.withResolvers());let{promise:t}=this.#n,n=()=>{this.#n?.reject(e?.reason??new Error("Aborted")),this.#n=null};return e?.addEventListener("abort",n,{once:!0}),t.finally(()=>{e?.removeEventListener("abort",n)})}#d(){this.#h()&&(this.#n?.resolve(),this.#n=null)}};function b(i){return i instanceof DOMException&&i.name==="InvalidStateError"}var h=class{id;ready;connection;#e=new Map;#r=new Set;#t;#s;#o=[];#n=!1;#c=!1;#i=!1;#a=!1;#h=Promise.withResolvers();#l=new Set;#d=f();constructor(e){this.id=e.remoteId,this.#t=e,this.#s=e.localId>e.remoteId,this.connection=new RTCPeerConnection(e.rtcConfig),this.ready=this.#h.promise,this.ready.catch(()=>{}),this.connection.onicecandidate=t=>{this.#i||!t.candidate||this.#t.onSignal({type:"candidate",candidate:t.candidate})},this.connection.onnegotiationneeded=()=>{this.#d(async()=>{if(!this.#i)try{this.#n=!0,await this.connection.setLocalDescription(),!this.#i&&this.connection.localDescription&&this.#t.onSignal({type:"description",description:this.connection.localDescription})}catch(t){b(t)||this.#t.onError(t)}finally{this.#n=!1}})},this.connection.onconnectionstatechange=()=>{if(this.connection.connectionState==="connected"&&this.#f(),this.connection.connectionState==="failed"||this.connection.connectionState==="closed"){if(this.#i)return;this.#t.onDisconnect()}},this.connection.ondatachannel=t=>this.#p(t.channel)}get channels(){return this.#e}createDataChannel(e,t){this.#r.add(e),!this.#s&&!this.#e.has(e)&&this.#p(this.connection.createDataChannel(e,t)),this.#f()}receiveSignal(e){this.#d(async()=>{if(!this.#i)try{if(e.type==="description"){let t=e.description.type==="offer"&&(this.#n||this.connection.signalingState!=="stable");if(this.#c=!this.#s&&t,this.#c)return;await this.connection.setRemoteDescription(e.description);for(let n of this.#o)await this.connection.addIceCandidate(n);this.#o=[],e.description.type==="offer"&&(await this.connection.setLocalDescription(),!this.#i&&this.connection.localDescription&&this.#t.onSignal({type:"description",description:this.connection.localDescription}));return}if(this.#c)return;this.connection.remoteDescription?await this.connection.addIceCandidate(e.candidate):this.#o.push(e.candidate)}catch(t){!this.#c&&!b(t)&&this.#t.onError(t)}})}open(e,t={}){if(this.#i)return Promise.reject(new Error("Peer closed"));if(t.signal?.aborted)return Promise.reject(t.signal.reason??new Error("Aborted"));let n=this.#e.get(e);if(n?.readyState==="open")return Promise.resolve(n);let{promise:s,resolve:o,reject:r}=Promise.withResolvers(),l=!1,E=(d,a)=>{l||(l=!0,this.#l.delete(w),t.signal?.removeEventListener("abort",C),a?o(a):r(d??new Error("Channel open failed")))},w=d=>{if(d){E(d);return}let a=this.#e.get(e);a?.readyState==="open"&&E(void 0,a)},C=()=>{E(t.signal?.reason??new Error("Aborted"))};return t.signal?.addEventListener("abort",C,{once:!0}),this.#l.add(w),w(),s}close(){if(!this.#i){this.#i=!0,this.#g(new Error("Peer closed")),this.connection.onicecandidate=null,this.connection.onnegotiationneeded=null,this.connection.onconnectionstatechange=null,this.connection.ondatachannel=null;for(let e of this.#e.values())e.close();this.#e.clear();try{this.connection.close()}catch{}}}#p(e){let t=this.#e.get(e.label),n=new c(e,this.connection);this.#e.set(e.label,n),t?.close(),n.ready.then(()=>{this.#v(),this.#f()},()=>this.#v()),this.#v()}#u(){if(this.#r.size===0)return this.connection.connectionState==="connected";for(let e of this.#r){let t=this.#e.get(e);if(!t||t.readyState!=="open")return!1}return!0}#f(){this.#a||this.#i||this.#u()&&(this.#a=!0,this.#h.resolve(this))}#v(){let e=this.#i?new Error("Peer closed"):void 0;for(let t of[...this.#l])t(e)}#g(e){this.#a||(this.#a=!0,this.#h.reject(e));for(let t of[...this.#l])t(e)}};var p=class extends Event{constructor(t){super("join");this.peer=t}peer},u=class extends Event{constructor(t){super("leave");this.id=t}id},g=class extends Event{constructor(t,n){super("error");this.id=t;this.error=n}id;error},y=class extends EventTarget{id;rtcConfig;#e=new Map;#r;#t=new Map;#s=new Map;#o=!1;constructor({id:e,signal:t,rtcConfig:n=S}){super(),this.id=e,this.#r=t,this.rtcConfig=n}get peers(){return this.#e}get(e){return this.#e.get(e)}channel(e,t={}){this.#n(),this.#t.set(e,t);for(let n of this.#e.values())n.createDataChannel(e,t)}connect(e,t={}){if(this.#n(),e===this.id)throw new Error("Cannot connect to self");let n=this.#e.get(e);if(n)return n;if(t.signal?.aborted)throw t.signal.reason??new Error("Aborted");let s=new h({localId:this.id,remoteId:e,rtcConfig:this.rtcConfig,onSignal:r=>this.#r(e,r),onError:r=>this.dispatchEvent(new g(e,r)),onDisconnect:()=>this.close(e)});this.#t.forEach((r,l)=>{s.createDataChannel(l,r)}),this.#e.set(e,s);let o=t.signal;if(o){let r=()=>this.close(e);o.addEventListener("abort",r),this.#s.set(e,()=>o.removeEventListener("abort",r))}return this.dispatchEvent(new p(s)),s}receive(e,t){this.#n();let n=this.connect(e);return n.receiveSignal(t),n}async send(e,t,n={}){this.#n();let s=[];for(let o of this.#e.values()){let r=o.channels.get(e);r?.readyState==="open"&&s.push(r.send(t,n))}s.length!==0&&await Promise.all(s)}async sendTo(e,t,n,s={}){this.#n();let o=this.#e.get(e);if(!o)throw new Error(`Unknown peer: ${e}`);let r=o.channels.get(t);if(!r)throw new Error(`No channel "${t}" for peer ${e}`);if(r.readyState!=="open")throw new Error(`Channel "${t}" with ${e} is not open`);await r.send(n,s)}close(e){let t=this.#e.get(e);t&&(this.#e.delete(e),this.#s.get(e)?.(),this.#s.delete(e),t.close(),this.dispatchEvent(new u(e)))}dispose(){if(!this.#o){this.#o=!0;for(let e of[...this.#e.keys()])this.close(e);this.#t.clear()}}[Symbol.dispose](){this.dispose()}addEventListener(e,t,n){super.addEventListener(e,t,n)}removeEventListener(e,t,n){super.removeEventListener(e,t,n)}#n(){if(this.#o)throw new Error("FastRTC disposed")}};export{c as Channel,v as ChannelErrorEvent,g as ErrorEvent,y as FastRTC,p as JoinEvent,u as LeaveEvent,h as Peer};
package/dist/peer.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { Channel } from "./channel";
2
+ export type SignalPayload = {
3
+ type: "description";
4
+ description: RTCSessionDescriptionInit;
5
+ } | {
6
+ type: "candidate";
7
+ candidate: RTCIceCandidateInit;
8
+ };
9
+ type PeerOptions = {
10
+ localId: string;
11
+ remoteId: string;
12
+ rtcConfig: RTCConfiguration;
13
+ onSignal: (payload: SignalPayload) => void;
14
+ onError: (error: unknown) => void;
15
+ onDisconnect: () => void;
16
+ };
17
+ export declare class Peer {
18
+ #private;
19
+ readonly id: string;
20
+ readonly ready: Promise<Peer>;
21
+ readonly connection: RTCPeerConnection;
22
+ constructor(opts: PeerOptions);
23
+ get channels(): ReadonlyMap<string, Channel>;
24
+ createDataChannel(label: string, options?: RTCDataChannelInit): void;
25
+ receiveSignal(signal: SignalPayload): void;
26
+ open(label: string, options?: {
27
+ signal?: AbortSignal;
28
+ }): Promise<Channel>;
29
+ close(): void;
30
+ }
31
+ export {};
package/dist/rtc.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { type SendOptions } from "./channel";
2
+ import { Peer, type SignalPayload } from "./peer";
3
+ export declare class JoinEvent extends Event {
4
+ readonly peer: Peer;
5
+ constructor(peer: Peer);
6
+ }
7
+ export declare class LeaveEvent extends Event {
8
+ readonly id: string;
9
+ constructor(id: string);
10
+ }
11
+ export declare class ErrorEvent extends Event {
12
+ readonly id: string;
13
+ readonly error: unknown;
14
+ constructor(id: string, error: unknown);
15
+ }
16
+ interface FastRTCEventMap {
17
+ join: JoinEvent;
18
+ leave: LeaveEvent;
19
+ error: ErrorEvent;
20
+ }
21
+ interface FastRTCOptions {
22
+ id: string;
23
+ signal: (to: string, payload: SignalPayload) => void;
24
+ rtcConfig?: RTCConfiguration;
25
+ }
26
+ export declare class FastRTC extends EventTarget {
27
+ #private;
28
+ readonly id: string;
29
+ readonly rtcConfig: RTCConfiguration;
30
+ constructor({ id, signal, rtcConfig }: FastRTCOptions);
31
+ get peers(): ReadonlyMap<string, Peer>;
32
+ get(id: string): Peer | undefined;
33
+ channel(label: string, options?: RTCDataChannelInit): void;
34
+ connect(remoteId: string, options?: {
35
+ signal?: AbortSignal;
36
+ }): Peer;
37
+ receive(remoteId: string, payload: SignalPayload): Peer;
38
+ send(label: string, data: string | BufferSource, options?: SendOptions): Promise<void>;
39
+ sendTo(peerId: string, label: string, data: string | BufferSource, options?: SendOptions): Promise<void>;
40
+ close(remoteId: string): void;
41
+ dispose(): void;
42
+ [Symbol.dispose](): void;
43
+ addEventListener<K extends keyof FastRTCEventMap>(type: K, listener: (this: FastRTC, ev: FastRTCEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void;
44
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void;
45
+ removeEventListener<K extends keyof FastRTCEventMap>(type: K, listener: (this: FastRTC, ev: FastRTCEventMap[K]) => void, options?: boolean | EventListenerOptions): void;
46
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void;
47
+ }
48
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fastrtc",
3
- "version": "1.1.0",
4
- "description": "Easy to use, zero-dependency WebRTC mesh library for browsers",
3
+ "version": "1.1.1",
4
+ "description": "Mesh helper for connecting peers and opening data channels",
5
5
  "keywords": [
6
6
  "datachannel",
7
7
  "fast",
@@ -1,7 +0,0 @@
1
- export declare class EventEmitter<TEvents extends object = Record<string, (...args: unknown[]) => void>> {
2
- #private;
3
- on<K extends keyof TEvents & string>(name: K, fn: TEvents[K]): () => void;
4
- has<K extends keyof TEvents & string>(name: K): boolean;
5
- emit<K extends keyof TEvents & string>(name: K, ...args: Parameters<TEvents[K] extends (...args: infer A) => unknown ? TEvents[K] : never>): void;
6
- destroy(): void;
7
- }
@@ -1,24 +0,0 @@
1
- import { EventEmitter } from "./event-emitter";
2
- import { type SendData, type SendOptions } from "./stream-source";
3
- import { TransferLimiter } from "./transfer-limiter";
4
- export type ChannelProgress = {
5
- direction: "send" | "receive";
6
- bytes: number;
7
- total?: number;
8
- };
9
- interface FastDataChannelEvents {
10
- open: () => void;
11
- close: () => void;
12
- message: (data: unknown) => void;
13
- error: (error: unknown) => void;
14
- stream: (stream: ReadableStream<Uint8Array>, meta?: unknown) => void;
15
- progress: (info: ChannelProgress) => void;
16
- }
17
- export declare class FastDataChannel extends EventEmitter<FastDataChannelEvents> {
18
- #private;
19
- readonly channel: RTCDataChannel;
20
- constructor(channel: RTCDataChannel, pc: RTCPeerConnection | null, limiter?: TransferLimiter);
21
- send(data: SendData, options?: SendOptions): Promise<void>;
22
- destroy(): void;
23
- }
24
- export {};
package/dist/framing.d.ts DELETED
@@ -1,5 +0,0 @@
1
- export declare function encodeFrame(flags: number, payload: Uint8Array): ArrayBuffer;
2
- export declare function parseFrame(data: ArrayBuffer): {
3
- flags: number;
4
- payload: Uint8Array;
5
- } | null;
@@ -1,29 +0,0 @@
1
- import { FastDataChannel } from "./fast-data-channel";
2
- import type { SignalPayload } from "./rtc-manager";
3
- import type { TransferLimiter } from "./transfer-limiter";
4
- type PeerNodeOptions = {
5
- localId: string;
6
- remoteId: string;
7
- rtcConfig: RTCConfiguration;
8
- limiter: TransferLimiter;
9
- onSignal: (payload: SignalPayload) => void;
10
- onError: (error: unknown) => void;
11
- onTrack: (track: MediaStreamTrack, stream: MediaStream) => void;
12
- onTrackEnded: (track: MediaStreamTrack, stream: MediaStream) => void;
13
- onChannel: (channel: FastDataChannel) => void;
14
- onConnected: () => void;
15
- onDisconnect: () => void;
16
- };
17
- export declare class PeerNode {
18
- #private;
19
- readonly remoteId: string;
20
- readonly channels: Map<string, FastDataChannel>;
21
- get isConnected(): boolean;
22
- constructor(opts: PeerNodeOptions);
23
- addTrack(track: MediaStreamTrack, stream: MediaStream): void;
24
- removeTrack(track: MediaStreamTrack): void;
25
- receiveSignal(signal: SignalPayload): void;
26
- createDataChannel(label: string, options?: RTCDataChannelInit): void;
27
- destroy(): void;
28
- }
29
- export {};
@@ -1,45 +0,0 @@
1
- import { EventEmitter } from "./event-emitter";
2
- import { FastDataChannel } from "./fast-data-channel";
3
- import { type SendData, type SendOptions } from "./stream-source";
4
- export type SignalPayload = {
5
- type: "description";
6
- description: RTCSessionDescriptionInit;
7
- } | {
8
- type: "candidate";
9
- candidate: RTCIceCandidateInit;
10
- };
11
- interface RTCManagerOptions {
12
- id: string;
13
- signal: (to: string, payload: SignalPayload) => void;
14
- rtcConfig?: RTCConfiguration;
15
- maxTransfers?: number;
16
- }
17
- interface RTCManagerEvents {
18
- join: (id: string) => void;
19
- leave: (id: string) => void;
20
- ready: (id: string) => void;
21
- channel: (id: string, channel: FastDataChannel) => void;
22
- track: (id: string, track: MediaStreamTrack, stream: MediaStream) => void;
23
- "track-ended": (id: string, track: MediaStreamTrack, stream: MediaStream) => void;
24
- error: (id: string, error: unknown) => void;
25
- }
26
- export declare class RTCManager extends EventEmitter<RTCManagerEvents> {
27
- #private;
28
- readonly id: string;
29
- readonly rtcConfig: RTCConfiguration;
30
- constructor({ id, signal, rtcConfig, maxTransfers, }: RTCManagerOptions);
31
- add(stream: MediaStream): void;
32
- remove(stream: MediaStream): void;
33
- channel(label: string, options?: RTCDataChannelInit): void;
34
- getChannel(peerId: string, label: string): FastDataChannel | undefined;
35
- whenOpen(peerId: string, label: string, options?: {
36
- signal?: AbortSignal;
37
- }): Promise<FastDataChannel>;
38
- connect(remoteId: string): void;
39
- receive(remoteId: string, payload: SignalPayload): void;
40
- sendTo(peerId: string, label: string, data: SendData, options?: SendOptions): Promise<void>;
41
- send(label: string, data: SendData, options?: SendOptions): Promise<void>;
42
- close(remoteId: string): void;
43
- destroy(): void;
44
- }
45
- export {};
@@ -1,12 +0,0 @@
1
- export type SendableData = string | Blob | ArrayBuffer | ArrayBufferView;
2
- export type SendData = string | Blob | BufferSource | ReadableStream<SendableData> | AsyncIterable<SendableData>;
3
- export type SendOptions = {
4
- signal?: AbortSignal;
5
- meta?: unknown;
6
- /** Blob/File only. Slice from this byte before streaming. */
7
- offset?: number;
8
- onProgress?: (bytes: number, total?: number) => void;
9
- };
10
- export declare function canCloneSource(data: SendData): boolean;
11
- export declare function toReadableStream(source: Exclude<SendData, string>): ReadableStream<SendableData>;
12
- export declare function normalizeChunk(value: SendableData): Uint8Array;
@@ -1,6 +0,0 @@
1
- export declare class TransferLimiter {
2
- #private;
3
- constructor(max: number);
4
- acquire(): Promise<() => void>;
5
- destroy(): void;
6
- }