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 +74 -118
- package/dist/channel.d.ts +29 -0
- package/dist/consts.d.ts +0 -8
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/peer.d.ts +31 -0
- package/dist/rtc.d.ts +48 -0
- package/package.json +2 -2
- package/dist/event-emitter.d.ts +0 -7
- package/dist/fast-data-channel.d.ts +0 -24
- package/dist/framing.d.ts +0 -5
- package/dist/peer-node.d.ts +0 -29
- package/dist/rtc-manager.d.ts +0 -45
- package/dist/stream-source.d.ts +0 -12
- package/dist/transfer-limiter.d.ts +0 -6
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# fastrtc
|
|
2
2
|
|
|
3
|
-
Browser WebRTC mesh
|
|
3
|
+
Browser WebRTC mesh helper. Zero runtime dependencies.
|
|
4
4
|
|
|
5
|
-
You wire signaling
|
|
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
|
-
│
|
|
19
|
-
│ send / sendTo
|
|
17
|
+
│ peer.ready │ declared channels open │
|
|
18
|
+
│ send / sendTo / ch.send │ string or BufferSource │
|
|
20
19
|
```
|
|
21
20
|
|
|
22
|
-
**
|
|
23
|
-
|
|
24
|
-
|
|
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 {
|
|
34
|
+
import { FastRTC } from "fastrtc";
|
|
40
35
|
import type { SignalPayload } from "fastrtc";
|
|
41
36
|
|
|
42
|
-
const rtc = new
|
|
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.
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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
|
-
##
|
|
79
|
-
|
|
80
|
-
### Setup
|
|
79
|
+
## Channels
|
|
81
80
|
|
|
82
81
|
```ts
|
|
83
82
|
rtc.channel("chat", { ordered: true });
|
|
84
|
-
rtc.channel("files"
|
|
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.
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
+
`string | BufferSource` only. No framing, no Blob/stream helpers.
|
|
112
94
|
|
|
113
|
-
|
|
95
|
+
Late labels after `ready`: `await bob.open("files")`.
|
|
114
96
|
|
|
115
|
-
|
|
97
|
+
## Broadcast
|
|
116
98
|
|
|
117
99
|
| Method | Target |
|
|
118
100
|
| ------ | ------ |
|
|
119
|
-
| `send(label, data)` | Every peer with
|
|
101
|
+
| `send(label, data)` | Every peer with that label open |
|
|
120
102
|
| `sendTo(peerId, label, data)` | One peer |
|
|
121
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
107
|
+
## Events
|
|
140
108
|
|
|
141
|
-
|
|
109
|
+
`FastRTC` is an `EventTarget`:
|
|
142
110
|
|
|
143
|
-
|
|
111
|
+
| Event | Payload |
|
|
112
|
+
| ----- | ------- |
|
|
113
|
+
| `join` | `peer` |
|
|
114
|
+
| `leave` | `id` |
|
|
115
|
+
| `error` | `id`, `error` |
|
|
144
116
|
|
|
145
|
-
|
|
117
|
+
`Channel` events: `message` (`data`), `close`, `error`.
|
|
146
118
|
|
|
147
|
-
|
|
119
|
+
ICE `disconnected` does not tear down (can recover). `failed` / `closed` emit `leave`.
|
|
148
120
|
|
|
149
121
|
## Media
|
|
150
122
|
|
|
151
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
### `
|
|
134
|
+
### `FastRTC`
|
|
180
135
|
|
|
181
136
|
```ts
|
|
182
|
-
new
|
|
137
|
+
new FastRTC({
|
|
183
138
|
id: string;
|
|
184
139
|
signal: (to: string, payload: SignalPayload) => void;
|
|
185
|
-
rtcConfig?: RTCConfiguration;
|
|
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
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
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
|
-
| `
|
|
200
|
-
| `
|
|
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
|
-
|
|
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
|
-
| `
|
|
217
|
-
| `
|
|
218
|
-
| `
|
|
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
|
-
|
|
172
|
+
### `Channel`
|
|
221
173
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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 {
|
|
2
|
-
export {
|
|
3
|
-
export
|
|
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.
|
|
4
|
-
"description": "
|
|
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",
|
package/dist/event-emitter.d.ts
DELETED
|
@@ -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
package/dist/peer-node.d.ts
DELETED
|
@@ -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 {};
|
package/dist/rtc-manager.d.ts
DELETED
|
@@ -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 {};
|
package/dist/stream-source.d.ts
DELETED
|
@@ -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;
|