fastrtc 1.0.1 → 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,152 +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 |
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`).
109
92
 
110
- Get a channel directly: `rtc.getChannel(peerId, "files")` or `await rtc.whenOpen(peerId, "files")`.
93
+ `string | BufferSource` only. No framing, no Blob/stream helpers.
111
94
 
112
- ### Sending
95
+ Late labels after `ready`: `await bob.open("files")`.
96
+
97
+ ## Broadcast
113
98
 
114
99
  | Method | Target |
115
100
  | ------ | ------ |
116
- | `send(label, data)` | Every peer with an open channel on `label` |
101
+ | `send(label, data)` | Every peer with that label open |
117
102
  | `sendTo(peerId, label, data)` | One peer |
118
- | `ch.send(data)` | When you already have the `FastDataChannel` |
119
-
120
- **Data types:**
103
+ | `channel.send(data)` | Channel you already have |
121
104
 
122
- - **String**single SCTP message `message` event
123
- - **Blob / File / BufferSource / ReadableStream / async iterable** → framed stream → `stream` event
124
-
125
- ```ts
126
- await rtc.send("files", file); // all peers
127
- await rtc.sendTo("bob", "files", file); // one peer
128
- await rtc.sendTo("bob", "files", file, {
129
- meta: { name: file.name }, // JSON on first frame
130
- offset: 1024, // Blob/File slice start
131
- onProgress: (bytes, total) => { /* ... */ },
132
- signal: abortController.signal,
133
- });
134
- ```
105
+ No open targets on `send` resolves, does not throw. Both honor `{ signal?: AbortSignal }`.
135
106
 
136
- **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
137
108
 
138
- **ReadableStream caveat:** one-shot streams work with `sendTo` only. `send()` to multiple peers throws — use a `Blob`/`File` instead.
109
+ `FastRTC` is an `EventTarget`:
139
110
 
140
- 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` |
141
116
 
142
- ### Receiving
117
+ `Channel` events: `message` (`data`), `close`, `error`.
143
118
 
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.
119
+ ICE `disconnected` does not tear down (can recover). `failed` / `closed` emit `leave`.
145
120
 
146
121
  ## Media
147
122
 
148
- ```ts
149
- const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
150
- rtc.add(stream); // tracks added before connect() attach to new peers automatically
151
-
152
- rtc.on("track", (peerId, track, stream) => {
153
- videoElement.srcObject = stream;
154
- });
155
-
156
- rtc.on("track-ended", (peerId, track) => { /* remote track stopped */ });
157
-
158
- rtc.remove(stream);
159
- ```
160
-
161
- ## Transfer limiter
162
-
163
- 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:
164
124
 
165
125
  ```ts
166
- const rtc = new RTCManager({ id: "alice", signal, maxTransfers: 4 });
167
- // Infinity = unlimited
126
+ bob.connection.addTrack(track, stream);
127
+ bob.connection.ontrack = (e) => {
128
+ video.srcObject = e.streams[0];
129
+ };
168
130
  ```
169
131
 
170
- Each outgoing framed send holds one slot until complete. Same-channel sends are serialized and do not hold a slot while waiting.
171
-
172
132
  ## API
173
133
 
174
- ### `RTCManager`
134
+ ### `FastRTC`
175
135
 
176
136
  ```ts
177
- new RTCManager({
137
+ new FastRTC({
178
138
  id: string;
179
139
  signal: (to: string, payload: SignalPayload) => void;
180
- rtcConfig?: RTCConfiguration; // default: Google STUN
181
- maxTransfers?: number; // default: 4
140
+ rtcConfig?: RTCConfiguration; // default: Google STUN
182
141
  })
183
142
  ```
184
143
 
185
144
  | Method | Description |
186
145
  | ------ | ----------- |
187
- | `connect(remoteId)` | Open connection to a peer |
188
- | `receive(remoteId, payload)` | Apply remote SDP/ICE |
189
146
  | `channel(label, options?)` | Register a labeled channel for all peers |
190
- | `getChannel(peerId, label)` | Existing channel, or `undefined` |
191
- | `whenOpen(peerId, label, { signal? })` | Promise for an open channel |
192
- | `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` |
193
151
  | `sendTo(peerId, label, data, options?)` | Send to one peer |
194
- | `add(stream)` / `remove(stream)` | Manage outgoing media tracks |
195
- | `close(remoteId)` | Tear down one peer |
196
- | `destroy()` | Close everything, reject queued transfers |
152
+ | `close(id)` | Tear down one peer |
153
+ | `dispose()` / `[Symbol.dispose]()` | Close everything |
197
154
 
198
155
  | Property | Description |
199
156
  | -------- | ----------- |
200
157
  | `id` | Local peer ID |
201
158
  | `rtcConfig` | Active RTC configuration |
159
+ | `peers` | `ReadonlyMap<string, Peer>` |
202
160
 
203
- **Events:** `join`, `ready`, `leave`, `channel`, `track`, `track-ended`. `.on(event, handler)` returns unsubscribe.
204
-
205
- ### `FastDataChannel`
206
-
207
- Wraps `RTCDataChannel` with streaming, backpressure, and typed events.
161
+ ### `Peer`
208
162
 
209
163
  | Method / property | Description |
210
164
  | ----------------- | ----------- |
211
- | `channel` | Underlying `RTCDataChannel` |
212
- | `send(data, options?)` | String message; else → framed stream |
213
- | `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 |
214
171
 
215
- **Events:** `open`, `close`, `message`, `error`, `stream`, `progress`.
172
+ ### `Channel`
216
173
 
217
- ### Exports
218
-
219
- `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 |
220
181
 
221
182
  ## Build
222
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,9 +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 FRAME_HEADER_SIZE = 2;
6
- export declare const RECEIVE_BACKLOG = 32;
7
1
  export declare const SCTP_DEFAULT_MESSAGE_SIZE = 65536;
8
2
  export declare const DEFAULT_RTC_CONFIG: RTCConfiguration;
9
- export declare const DEFAULT_MAX_TRANSFERS = 4;
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 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 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 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 {};
@@ -0,0 +1 @@
1
+ export declare function createQueue(): (job: () => Promise<void>) => Promise<void>;
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.0.1",
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",
@@ -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
  }
@@ -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, transport: RTCSctpTransport | 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,28 +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
- onTrack: (track: MediaStreamTrack, stream: MediaStream) => void;
11
- onTrackEnded: (track: MediaStreamTrack, stream: MediaStream) => void;
12
- onChannel: (channel: FastDataChannel) => void;
13
- onConnected: () => void;
14
- onDisconnect: () => void;
15
- };
16
- export declare class PeerNode {
17
- #private;
18
- readonly remoteId: string;
19
- readonly channels: Map<string, FastDataChannel>;
20
- get isConnected(): boolean;
21
- constructor(opts: PeerNodeOptions);
22
- addTrack(track: MediaStreamTrack, stream: MediaStream): void;
23
- removeTrack(track: MediaStreamTrack): void;
24
- receiveSignal(signal: SignalPayload): Promise<void>;
25
- createDataChannel(label: string, options?: RTCDataChannelInit): void;
26
- destroy(): void;
27
- }
28
- export {};
@@ -1,44 +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
- }
25
- export declare class RTCManager extends EventEmitter<RTCManagerEvents> {
26
- #private;
27
- readonly id: string;
28
- readonly rtcConfig: RTCConfiguration;
29
- constructor({ id, signal, rtcConfig, maxTransfers, }: RTCManagerOptions);
30
- add(stream: MediaStream): void;
31
- remove(stream: MediaStream): void;
32
- channel(label: string, options?: RTCDataChannelInit): void;
33
- getChannel(peerId: string, label: string): FastDataChannel | undefined;
34
- whenOpen(peerId: string, label: string, options?: {
35
- signal?: AbortSignal;
36
- }): Promise<FastDataChannel>;
37
- connect(remoteId: string): void;
38
- receive(remoteId: string, payload: SignalPayload): void;
39
- sendTo(peerId: string, label: string, data: SendData, options?: SendOptions): Promise<void>;
40
- send(label: string, data: SendData, options?: SendOptions): Promise<void>;
41
- close(remoteId: string): void;
42
- destroy(): void;
43
- }
44
- 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
- }