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 +75 -114
- package/dist/channel.d.ts +29 -0
- package/dist/consts.d.ts +0 -7
- 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/queue.d.ts +1 -0
- package/dist/rtc.d.ts +48 -0
- package/package.json +7 -4
- 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 -28
- package/dist/rtc-manager.d.ts +0 -44
- 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,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
|
|
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 |
|
|
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
|
-
|
|
93
|
+
`string | BufferSource` only. No framing, no Blob/stream helpers.
|
|
111
94
|
|
|
112
|
-
|
|
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
|
|
101
|
+
| `send(label, data)` | Every peer with that label open |
|
|
117
102
|
| `sendTo(peerId, label, data)` | One peer |
|
|
118
|
-
| `
|
|
119
|
-
|
|
120
|
-
**Data types:**
|
|
103
|
+
| `channel.send(data)` | Channel you already have |
|
|
121
104
|
|
|
122
|
-
|
|
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
|
-
|
|
107
|
+
## Events
|
|
137
108
|
|
|
138
|
-
|
|
109
|
+
`FastRTC` is an `EventTarget`:
|
|
139
110
|
|
|
140
|
-
|
|
111
|
+
| Event | Payload |
|
|
112
|
+
| ----- | ------- |
|
|
113
|
+
| `join` | `peer` |
|
|
114
|
+
| `leave` | `id` |
|
|
115
|
+
| `error` | `id`, `error` |
|
|
141
116
|
|
|
142
|
-
|
|
117
|
+
`Channel` events: `message` (`data`), `close`, `error`.
|
|
143
118
|
|
|
144
|
-
|
|
119
|
+
ICE `disconnected` does not tear down (can recover). `failed` / `closed` emit `leave`.
|
|
145
120
|
|
|
146
121
|
## Media
|
|
147
122
|
|
|
148
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
### `
|
|
134
|
+
### `FastRTC`
|
|
175
135
|
|
|
176
136
|
```ts
|
|
177
|
-
new
|
|
137
|
+
new FastRTC({
|
|
178
138
|
id: string;
|
|
179
139
|
signal: (to: string, payload: SignalPayload) => void;
|
|
180
|
-
rtcConfig?: RTCConfiguration;
|
|
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
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
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
|
-
| `
|
|
195
|
-
| `
|
|
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
|
-
|
|
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
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
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
|
-
|
|
172
|
+
### `Channel`
|
|
216
173
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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 {
|
|
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 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 {};
|
package/dist/queue.d.ts
ADDED
|
@@ -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.
|
|
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",
|
|
@@ -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": "
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest"
|
|
36
39
|
}
|
|
37
40
|
}
|
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, 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
package/dist/peer-node.d.ts
DELETED
|
@@ -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 {};
|
package/dist/rtc-manager.d.ts
DELETED
|
@@ -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 {};
|
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;
|