@trystero-p2p/torrent 0.26.0 β 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -1033
- package/dist/index.d.mts +4 -7
- package/dist/index.mjs +247 -145
- package/package.json +11 -2
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,100 +1,33 @@
|
|
|
1
1
|
# β¨π€β¨ Trystero
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Instant peer-to-peer multiplayer for the web β no server required.**
|
|
4
4
|
|
|
5
5
|
π **[Try it live on trystero.dev](https://trystero.dev)** π
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
Making your app work with multiple users usually means a detour into setting up
|
|
8
|
+
servers, auth, billing, databases, etc. β all so two browsers can say hello.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- ππ£ Rooms / broadcasting
|
|
17
|
-
- π’π© Automatic serialization / deserialization of data
|
|
18
|
-
- π₯π· Attach metadata to binary data and media streams
|
|
19
|
-
- βοΈβ³ Automatic chunking and throttling of large data
|
|
20
|
-
- β±π€ Progress events and promises for data transfers
|
|
21
|
-
- ππ Session data encryption
|
|
22
|
-
- πβ‘ Can run peers server-side on Node, Bun, and Deno
|
|
23
|
-
- βοΈπͺ React hooks
|
|
24
|
-
|
|
25
|
-
You can see what people are building with Trystero
|
|
26
|
-
[here](https://github.com/jeremyckahn/awesome-trystero).
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
## Contents
|
|
31
|
-
|
|
32
|
-
- [How it works](#how-it-works)
|
|
33
|
-
- [Get started](#get-started)
|
|
34
|
-
- [Listen for events](#listen-for-events)
|
|
35
|
-
- [Broadcast events](#broadcast-events)
|
|
36
|
-
- [Audio and video](#audio-and-video)
|
|
37
|
-
- [Advanced](#advanced)
|
|
38
|
-
- [Binary metadata](#binary-metadata)
|
|
39
|
-
- [Receiving files](#receiving-files)
|
|
40
|
-
- [Action promises](#action-promises)
|
|
41
|
-
- [Progress updates](#progress-updates)
|
|
42
|
-
- [Encryption](#encryption)
|
|
43
|
-
- [React hooks](#react-hooks)
|
|
44
|
-
- [Troubleshooting connection issues](#troubleshooting-connection-issues)
|
|
45
|
-
- [Running server-side (Node, Bun, Deno)](#running-server-side-node-bun-deno)
|
|
46
|
-
- [Self-hosted WebSocket relay](#self-hosted-websocket-relay)
|
|
47
|
-
- [Write your own strategy](#write-your-own-strategy)
|
|
48
|
-
- [Supabase setup](#supabase-setup)
|
|
49
|
-
- [Firebase setup](#firebase-setup)
|
|
50
|
-
- [API](#api)
|
|
51
|
-
- [`joinRoom(config, roomId, [callbacks])`](#joinroomconfig-roomid-callbacks)
|
|
52
|
-
- [`selfId`](#selfid)
|
|
53
|
-
- [`getRelaySockets()`](#getrelaysockets)
|
|
54
|
-
- [`pauseRelayReconnection()`](#pauserelayreconnection)
|
|
55
|
-
- [`resumeRelayReconnection()`](#resumerelayreconnection)
|
|
56
|
-
- [Which strategy should I choose?](#which-strategy-should-i-choose)
|
|
10
|
+
**Trystero skips the backend entirely.** Import the library, join a room by
|
|
11
|
+
name, and browsers discover each other automatically and connect directly over
|
|
12
|
+
fast, end-to-end encrypted WebRTC. Out of the box, you get verified user
|
|
13
|
+
identities, synced user and room state, host permissions, RPC, chunked file
|
|
14
|
+
transfers, and live audio/video. The web was meant to be decentralized and
|
|
15
|
+
Trystero makes it easy to build apps where data flows without middlemen.
|
|
57
16
|
|
|
58
17
|
---
|
|
59
18
|
|
|
60
|
-
##
|
|
61
|
-
|
|
62
|
-
π **If you just want to try out Trystero, you can skip this explainer and
|
|
63
|
-
[jump into using it](#get-started).**
|
|
64
|
-
|
|
65
|
-
To establish a direct peer-to-peer connection with WebRTC, a signalling channel
|
|
66
|
-
is needed to exchange peer information
|
|
67
|
-
([SDP](https://en.wikipedia.org/wiki/Session_Description_Protocol)). Typically
|
|
68
|
-
this involves running your own matchmaking server but Trystero abstracts this
|
|
69
|
-
away for you and offers multiple strategies for connecting peers (currently
|
|
70
|
-
BitTorrent, Nostr, MQTT, Supabase, Firebase, IPFS, and self-hosted WebSocket
|
|
71
|
-
relay).
|
|
19
|
+
## Quickstart
|
|
72
20
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
> π
|
|
76
|
-
>
|
|
77
|
-
> Beyond peer discovery, your app's data never touches the strategy medium and
|
|
78
|
-
> is sent directly peer-to-peer and end-to-end encrypted between users.
|
|
79
|
-
>
|
|
80
|
-
> π
|
|
81
|
-
|
|
82
|
-
You can [compare strategies here](#which-strategy-should-i-choose).
|
|
83
|
-
|
|
84
|
-
## Get started
|
|
85
|
-
|
|
86
|
-
Install Trystero with your preferred package manager, then import it in your
|
|
87
|
-
code:
|
|
21
|
+
Install Trystero from npm:
|
|
88
22
|
|
|
89
23
|
```sh
|
|
90
24
|
npm i trystero
|
|
91
25
|
```
|
|
92
26
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
No package manager? You can also use a CDN:
|
|
27
|
+
<details>
|
|
28
|
+
<summary>
|
|
29
|
+
(Or skip the build step and import from a script tag)
|
|
30
|
+
</summary>
|
|
98
31
|
|
|
99
32
|
```html
|
|
100
33
|
<script type="module">
|
|
@@ -102,1066 +35,337 @@ No package manager? You can also use a CDN:
|
|
|
102
35
|
</script>
|
|
103
36
|
```
|
|
104
37
|
|
|
105
|
-
|
|
106
|
-
other stategy by changing which package you import:
|
|
107
|
-
|
|
108
|
-
```js
|
|
109
|
-
import {joinRoom} from '@trystero-p2p/mqtt'
|
|
110
|
-
// or
|
|
111
|
-
import {joinRoom} from '@trystero-p2p/torrent'
|
|
112
|
-
// or
|
|
113
|
-
import {joinRoom} from '@trystero-p2p/supabase'
|
|
114
|
-
// or
|
|
115
|
-
import {joinRoom} from '@trystero-p2p/firebase'
|
|
116
|
-
// or
|
|
117
|
-
import {joinRoom} from '@trystero-p2p/ipfs'
|
|
118
|
-
// or
|
|
119
|
-
import {joinRoom} from '@trystero-p2p/ws-relay'
|
|
120
|
-
```
|
|
38
|
+
</details>
|
|
121
39
|
|
|
122
|
-
|
|
40
|
+
Join a room with a unique app ID and room name and start syncing state. Here's a
|
|
41
|
+
complete multiplayer cursor canvas in a dozen lines:
|
|
123
42
|
|
|
124
43
|
```js
|
|
125
|
-
|
|
126
|
-
const room = joinRoom(config, 'yoyodyne')
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
The first argument is a configuration object that requires an `appId`. This
|
|
130
|
-
should be a completely unique identifier for your appΒΉ. The second argument is
|
|
131
|
-
the room ID.
|
|
132
|
-
|
|
133
|
-
> Why rooms? Browsers can only handle a limited amount of WebRTC connections at
|
|
134
|
-
> a time so it's recommended to design your app such that users are divided into
|
|
135
|
-
> groups (or rooms, or namespaces, or channels... whatever you'd like to call
|
|
136
|
-
> them).
|
|
44
|
+
import {joinRoom} from 'trystero'
|
|
137
45
|
|
|
138
|
-
|
|
139
|
-
|
|
46
|
+
// No API keys, accounts, or backend setup. Just pick a globally unique appId
|
|
47
|
+
// (e.g. your app's domain or 'username/app-name') and a room name
|
|
48
|
+
const room = joinRoom({appId: 'pixel-party.example.com'}, 'studio-49')
|
|
140
49
|
|
|
141
|
-
|
|
50
|
+
// Give every user their own signed state slot
|
|
51
|
+
const cursor = room.makeUserState('cursor', {x: 0, y: 0})
|
|
142
52
|
|
|
143
|
-
|
|
53
|
+
window.addEventListener('pointermove', e =>
|
|
54
|
+
cursor.set({x: e.clientX, y: e.clientY})
|
|
55
|
+
)
|
|
144
56
|
|
|
145
|
-
|
|
146
|
-
|
|
57
|
+
cursor.onChange((pos, {userId, self, removed}) => {
|
|
58
|
+
if (self) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
if (removed) {
|
|
62
|
+
removeCursor(userId)
|
|
63
|
+
} else {
|
|
64
|
+
drawCursor(userId, pos)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
147
67
|
```
|
|
148
68
|
|
|
149
|
-
|
|
69
|
+
Open your page in two browsers anywhere in the world and watch the cursors move.
|
|
150
70
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
71
|
+
> **Local testing tip:** Tabs in the same browser profile share your `userId`
|
|
72
|
+
> automatically. Open a Private/Incognito window (or pass `identity: 'none'` in
|
|
73
|
+
> the room config) when simulating a second person on one machine.
|
|
154
74
|
|
|
155
|
-
|
|
75
|
+
---
|
|
156
76
|
|
|
157
|
-
|
|
158
|
-
room.onPeerStream = (stream, peerId) =>
|
|
159
|
-
(peerElements[peerId].video.srcObject = stream)
|
|
160
|
-
```
|
|
77
|
+
## What you can build with it
|
|
161
78
|
|
|
162
|
-
|
|
79
|
+
### 1. Synced user & room state
|
|
163
80
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
81
|
+
Most multiplayer apps need two kinds of live state: **per-user state** that each
|
|
82
|
+
participant owns (names, avatars, ready flags, cursors) and **shared room
|
|
83
|
+
state** for the group (a game board, round timer, or watch-party queue).
|
|
167
84
|
|
|
168
|
-
|
|
85
|
+
Trystero builds both right onto `room`. Updates apply immediately locally,
|
|
86
|
+
coalesce rapid writes on the wire, verify who wrote them, and sync automatically
|
|
87
|
+
to late joiners β even if the person who last updated shared state has already
|
|
88
|
+
closed their laptop.
|
|
169
89
|
|
|
170
90
|
```js
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
```
|
|
91
|
+
// Each player controls only their own signed slot β nobody can overwrite yours
|
|
92
|
+
const profile = room.makeUserState('profile', {name: 'Anon', ready: false})
|
|
93
|
+
profile.set({name: 'Oedipa', ready: true})
|
|
175
94
|
|
|
176
|
-
|
|
95
|
+
// Snapshot of everyone currently in the room: { [userId]: { name, ready } }
|
|
96
|
+
console.log(profile.getAll())
|
|
177
97
|
|
|
178
|
-
|
|
98
|
+
// Shared state for the whole room, synced automatically to latecomers
|
|
99
|
+
const track = room.makeState('track', {url: 'chill-beat.mp3', paused: true})
|
|
179
100
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
video: true
|
|
101
|
+
track.onChange((next, {userId}) => {
|
|
102
|
+
syncAudioPlayer(next)
|
|
103
|
+
console.log(`Updated by ${userId}`)
|
|
184
104
|
})
|
|
185
|
-
room.addStream(stream)
|
|
186
|
-
```
|
|
187
105
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
```js
|
|
191
|
-
const drink = room.makeAction('drink')
|
|
106
|
+
await track.set(prev => ({...prev, paused: false}))
|
|
107
|
+
```
|
|
192
108
|
|
|
193
|
-
|
|
194
|
-
|
|
109
|
+
Using React? Both `makeUserState` and `makeState` plug straight into
|
|
110
|
+
`useSyncExternalStore` β no `useEffect` glue or manual join/leave maps:
|
|
195
111
|
|
|
196
|
-
|
|
197
|
-
|
|
112
|
+
```jsx
|
|
113
|
+
import {useSyncExternalStore} from 'react'
|
|
198
114
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
115
|
+
function Lobby() {
|
|
116
|
+
const players = useSyncExternalStore(profile.onChange, profile.getAll)
|
|
117
|
+
const nowPlaying = useSyncExternalStore(track.onChange, track.get)
|
|
118
|
+
// render directly from `players` and `nowPlaying`
|
|
119
|
+
}
|
|
204
120
|
```
|
|
205
121
|
|
|
206
|
-
|
|
122
|
+
π **[Shared & Per-User State guide β](https://github.com/dmotz/trystero/blob/main/docs/state.md)**
|
|
207
123
|
|
|
208
|
-
|
|
209
|
-
const isEven = room.makeAction('is-even', {
|
|
210
|
-
kind: 'request',
|
|
211
|
-
onRequest: n => n % 2 === 0
|
|
212
|
-
})
|
|
124
|
+
---
|
|
213
125
|
|
|
214
|
-
|
|
215
|
-
target: friendId,
|
|
216
|
-
timeoutMs: 1000
|
|
217
|
-
})
|
|
218
|
-
```
|
|
126
|
+
### 2. Unforgeable user identity, passwords & room caps
|
|
219
127
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
128
|
+
Every participant gets a persistent, cryptographically verified `userId` out of
|
|
129
|
+
the box β no login screens or auth servers required. If someone refreshes the
|
|
130
|
+
page or opens your app in two tabs at once, Trystero recognizes them as the same
|
|
131
|
+
user automatically. (Want strictly throwaway IDs or keys loaded from your own
|
|
132
|
+
backend? One config option switches modes.)
|
|
223
133
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
kind: 'request',
|
|
227
|
-
onRequest: ({date}) => calendar.isFree(date)
|
|
228
|
-
})
|
|
134
|
+
Combine `password` and `maxUsers` to spin up private, seat-capped matches like a
|
|
135
|
+
2-player chess table or a 4-player co-op lobby:
|
|
229
136
|
|
|
230
|
-
|
|
231
|
-
|
|
137
|
+
```js
|
|
138
|
+
const room = joinRoom(
|
|
232
139
|
{
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
140
|
+
appId: 'chess.example.com',
|
|
141
|
+
password: 'knight-takes-f7-secret', // encrypts discovery so outsiders stay out
|
|
142
|
+
maxUsers: 2 // cap this match at 2 unique players (you + 1 opponent)
|
|
143
|
+
},
|
|
144
|
+
'table-7'
|
|
241
145
|
)
|
|
242
146
|
|
|
243
|
-
const
|
|
244
|
-
.filter(result => result.status === 'fulfilled' && result.value)
|
|
245
|
-
.map(result => result.peerId)
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
If you're using TypeScript, you can add a type hint to the action:
|
|
147
|
+
const {userId} = await room.getSelf()
|
|
249
148
|
|
|
250
|
-
|
|
251
|
-
|
|
149
|
+
room.onUserJoin(({userId}) => console.log(`Opponent ${userId} sat down`))
|
|
150
|
+
room.onUserLeave(({userId}) => console.log(`Opponent ${userId} left`))
|
|
252
151
|
|
|
253
|
-
|
|
152
|
+
room.onError(err => {
|
|
153
|
+
if (err.code === 'room-full' && Object.keys(room.getPeers()).length === 0) {
|
|
154
|
+
showBanner('Sorry, both seats at this table are taken!')
|
|
155
|
+
}
|
|
156
|
+
})
|
|
254
157
|
```
|
|
255
158
|
|
|
256
|
-
|
|
159
|
+
π **[Identity, Presence & Room Access guide β](https://github.com/dmotz/trystero/blob/main/docs/identity.md)**
|
|
257
160
|
|
|
258
|
-
|
|
259
|
-
const pic = room.makeAction('pic')
|
|
161
|
+
---
|
|
260
162
|
|
|
261
|
-
|
|
262
|
-
canvas.toBlob(blob => pic.send(blob))
|
|
163
|
+
### 3. Serverless hosts, custom roles & moderation
|
|
263
164
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
pic.onMessage = (data, {peerId}) =>
|
|
267
|
-
(imgs[peerId].src = URL.createObjectURL(new Blob([data])))
|
|
268
|
-
```
|
|
165
|
+
In a typical P2P app, everyone is equal β which gets awkward the moment someone
|
|
166
|
+
opens DevTools in your game and awards themselves a million points.
|
|
269
167
|
|
|
270
|
-
|
|
168
|
+
Import the tree-shakeable `roles()` plugin to create rooms with mathematically
|
|
169
|
+
verified admins, custom roles (like `'dj'` or `'moderator'`), authoritative
|
|
170
|
+
state that only authorized roles can edit, and peer banning β still with zero
|
|
171
|
+
backend:
|
|
271
172
|
|
|
272
173
|
```js
|
|
273
|
-
|
|
274
|
-
const name = room.makeAction('name')
|
|
174
|
+
import {joinRoom, roles} from 'trystero'
|
|
275
175
|
|
|
276
|
-
//
|
|
277
|
-
room
|
|
176
|
+
// Host creates a room where their userId has verified admin authority
|
|
177
|
+
const room = joinRoom(
|
|
178
|
+
{appId: 'oedipa/friday-trivia', use: [roles({selfAsAdmin: true})]},
|
|
179
|
+
'quiz-night'
|
|
180
|
+
)
|
|
278
181
|
|
|
279
|
-
//
|
|
280
|
-
|
|
182
|
+
// Share `roomId` (e.g. "quiz-night~9fX2kLm...") in an invite link;
|
|
183
|
+
// guests join with `use: [roles()]` and automatically verify the host
|
|
184
|
+
const {roomId} = await room.getSelf()
|
|
281
185
|
|
|
282
|
-
//
|
|
283
|
-
|
|
186
|
+
// With roles() enabled, makeState defaults to admin-only writes β
|
|
187
|
+
// everyone can read `game`, and peers reject forged edits automatically
|
|
188
|
+
const game = room.makeState('game', {round: 1, prompt: 'Ready?'})
|
|
189
|
+
await game.set({round: 2, prompt: 'Name the moons of Mars'})
|
|
284
190
|
|
|
285
|
-
|
|
286
|
-
|
|
191
|
+
// Promote helpers or boot troublemakers
|
|
192
|
+
await room.grantRole(friendId, 'cohost')
|
|
193
|
+
await room.ban(grieferId, {reason: 'spoiling answers'})
|
|
287
194
|
```
|
|
288
195
|
|
|
289
|
-
|
|
290
|
-
> scenes. This means you can send very large files and whatever data you send
|
|
291
|
-
> will be received on the other side as the same type (a number as a number, a
|
|
292
|
-
> string as a string, an object as an object, binary as binary, etc.). See
|
|
293
|
-
> [receiving files](#receiving-files) for optional size limits and acceptance
|
|
294
|
-
> callbacks.
|
|
295
|
-
|
|
296
|
-
## Audio and video
|
|
297
|
-
|
|
298
|
-
Here's a simple example of how you could create an audio chatroom:
|
|
196
|
+
π **[Admins, Roles & Moderation guide β](https://github.com/dmotz/trystero/blob/main/docs/roles.md)**
|
|
299
197
|
|
|
300
|
-
|
|
301
|
-
// this object can store audio instances for later
|
|
302
|
-
const peerAudios = {}
|
|
303
|
-
|
|
304
|
-
// get a local audio stream from the microphone
|
|
305
|
-
const selfStream = await navigator.mediaDevices.getUserMedia({
|
|
306
|
-
audio: true,
|
|
307
|
-
video: false
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
// send stream to peers currently in the room
|
|
311
|
-
room.addStream(selfStream)
|
|
312
|
-
|
|
313
|
-
// send stream to peers who join later
|
|
314
|
-
room.onPeerJoin = peerId => room.addStream(selfStream, {target: peerId})
|
|
315
|
-
|
|
316
|
-
// handle streams from other peers
|
|
317
|
-
room.onPeerStream = (stream, peerId) => {
|
|
318
|
-
// create an audio instance and set the incoming stream
|
|
319
|
-
const audio = new Audio()
|
|
320
|
-
audio.srcObject = stream
|
|
321
|
-
audio.autoplay = true
|
|
198
|
+
---
|
|
322
199
|
|
|
323
|
-
|
|
324
|
-
// later (volume, etc.)
|
|
325
|
-
peerAudios[peerId] = audio
|
|
326
|
-
}
|
|
327
|
-
```
|
|
200
|
+
### 4. Events, RPC & direct P2P file drop
|
|
328
201
|
|
|
329
|
-
|
|
330
|
-
|
|
202
|
+
For ephemeral events, request/response calls, and binary transfers, create typed
|
|
203
|
+
channels with `room.makeAction()`. Pass strings, JSON objects, `Blob`s, `File`s,
|
|
204
|
+
or `TypedArray`s β Trystero handles serialization, chunking, backpressure,
|
|
205
|
+
metadata headers, and transfer progress automatically, with zero cloud storage
|
|
206
|
+
bills:
|
|
331
207
|
|
|
332
208
|
```js
|
|
333
|
-
const
|
|
334
|
-
const videoContainer = document.getElementById('videos')
|
|
335
|
-
|
|
336
|
-
room.onPeerStream = (stream, peerId) => {
|
|
337
|
-
let video = peerVideos[peerId]
|
|
338
|
-
|
|
339
|
-
// if this peer hasn't sent a stream before, create a video element
|
|
340
|
-
if (!video) {
|
|
341
|
-
video = document.createElement('video')
|
|
342
|
-
video.autoplay = true
|
|
343
|
-
|
|
344
|
-
// add video element to the DOM
|
|
345
|
-
videoContainer.appendChild(video)
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
video.srcObject = stream
|
|
349
|
-
peerVideos[peerId] = video
|
|
350
|
-
}
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
## Advanced
|
|
209
|
+
const drop = room.makeAction('file-drop')
|
|
354
210
|
|
|
355
|
-
|
|
211
|
+
// Optionally add a predicate too accept based on max byteLength
|
|
212
|
+
drop.onReceive(({byteLength}) => byteLength <= 100 * 1024 ** 2)
|
|
356
213
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
interpreted. Instead of manually adding metadata bytes to the buffer you can
|
|
360
|
-
simply pass a metadata argument in the sender action for your binary payload:
|
|
361
|
-
|
|
362
|
-
```js
|
|
363
|
-
const file = room.makeAction('file')
|
|
214
|
+
// Listen for progress events to show a loading bar
|
|
215
|
+
drop.onReceiveProgress((pct, {metadata}) => showDownloadBar(metadata.name, pct))
|
|
364
216
|
|
|
365
|
-
file
|
|
366
|
-
|
|
367
|
-
`got a file (${metadata.name}) from ${peerId} with type ${metadata.type}`,
|
|
368
|
-
data
|
|
369
|
-
)
|
|
217
|
+
// When the file is done being received, save it
|
|
218
|
+
drop.onMessage((bytes, {userId, metadata}) => saveFile(bytes, metadata.name))
|
|
370
219
|
|
|
371
|
-
file
|
|
372
|
-
|
|
220
|
+
// Send a file to the room with optional metadata
|
|
221
|
+
await drop.send(file, {
|
|
222
|
+
metadata: {name: file.name},
|
|
223
|
+
onProgress: pct => showUploadBar(pct)
|
|
373
224
|
})
|
|
374
|
-
```
|
|
375
225
|
|
|
376
|
-
|
|
226
|
+
// Create a request action where you ask a peer for something and they provide
|
|
227
|
+
// an answer
|
|
228
|
+
const roll = room.makeAction('roll-check', {kind: 'request'})
|
|
377
229
|
|
|
378
|
-
|
|
379
|
-
|
|
230
|
+
// Respond to roll requests from other peers
|
|
231
|
+
roll.onRequest(() => Math.ceil(Math.random() * 20))
|
|
380
232
|
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
onReceive: ({byteLength}) => byteLength <= 100 * 1024 ** 2,
|
|
384
|
-
onMessage: (data, {metadata}) => saveFile(data, metadata.name)
|
|
385
|
-
})
|
|
233
|
+
// Ask a specific friendId for their roll number
|
|
234
|
+
const result = await roll.request(null, {target: friendId, timeoutMs: 2000})
|
|
386
235
|
```
|
|
387
236
|
|
|
388
|
-
|
|
389
|
-
approval before sending their contents; declined small messages are discarded
|
|
390
|
-
after arrival. You can also assign `file.onReceive` later or set it to `null` to
|
|
391
|
-
restore automatic acceptance.
|
|
392
|
-
|
|
393
|
-
`byteLength` is the payload size in bytes (UTF-8 for strings/JSON), excluding
|
|
394
|
-
metadata. The context's `signal` aborts when the transfer ends; use it to
|
|
395
|
-
dismiss pending approval UI.
|
|
237
|
+
π **[Actions, RPC & File Transfers guide β](https://github.com/dmotz/trystero/blob/main/docs/actions.md)**
|
|
396
238
|
|
|
397
|
-
|
|
398
|
-
room config to allow larger files or reduce memory use. Concurrent transfers
|
|
399
|
-
queue automatically when the budget is occupied. Received files must fit in
|
|
400
|
-
memory.
|
|
401
|
-
|
|
402
|
-
### Action promises
|
|
403
|
-
|
|
404
|
-
Sender promises resolve when local sending finishes; they do not confirm
|
|
405
|
-
delivery or application handling. If a receiver declines a large transfer, the
|
|
406
|
-
promise rejects with `error.kind === 'rejected'`. Use request actions when you
|
|
407
|
-
need a response.
|
|
408
|
-
|
|
409
|
-
```js
|
|
410
|
-
await file.send(amplePayload)
|
|
411
|
-
console.log('done sending to all peers')
|
|
412
|
-
```
|
|
239
|
+
---
|
|
413
240
|
|
|
414
|
-
###
|
|
241
|
+
### 5. Voice, video & screen sharing
|
|
415
242
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
percentage value between 0 and 1 and the receiving peer's ID:
|
|
243
|
+
Drop live audio, webcam video, screen captures, or Web Audio streams into any
|
|
244
|
+
room, and tag streams with `metadata` so peers can tell a facecam from a screen
|
|
245
|
+
share:
|
|
420
246
|
|
|
421
247
|
```js
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
onProgress: (percent, {peerId}) => (loadingBars[peerId].value = percent)
|
|
248
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
249
|
+
audio: true,
|
|
250
|
+
video: true
|
|
426
251
|
})
|
|
427
|
-
```
|
|
428
252
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
253
|
+
// Send to everyone here now, plus anyone who joins later
|
|
254
|
+
await room.addStream(stream, {metadata: {type: 'webcam'}})
|
|
255
|
+
room.onUserJoin(({userId}) =>
|
|
256
|
+
room.addStream(stream, {target: userId, metadata: {type: 'webcam'}})
|
|
257
|
+
)
|
|
433
258
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
259
|
+
// Add a video when receiving a stream from someone else
|
|
260
|
+
room.onPeerStream((remoteStream, {userId, metadata}) =>
|
|
261
|
+
attachVideo(userId, remoteStream, metadata.type)
|
|
262
|
+
)
|
|
438
263
|
```
|
|
439
264
|
|
|
440
|
-
|
|
441
|
-
receiving user that there is a transfer in progress with perhaps the name of the
|
|
442
|
-
incoming file.
|
|
265
|
+
π **[Audio, Video & Screen Sharing guide β](https://github.com/dmotz/trystero/blob/main/docs/media.md)**
|
|
443
266
|
|
|
444
|
-
|
|
445
|
-
metadata to differentiate between them, e.g. by sending a unique ID.
|
|
267
|
+
---
|
|
446
268
|
|
|
447
|
-
###
|
|
269
|
+
### 6. Scale to large numbers of peers
|
|
448
270
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
the chosen peering strategy medium. By default the SDP is encrypted using a key
|
|
453
|
-
derived from your app ID and room ID to prevent plaintext session data from
|
|
454
|
-
appearing in logs. This is fine for most use cases, however a relay strategy
|
|
455
|
-
operator can reverse engineer the key using the room and app IDs. A more secure
|
|
456
|
-
option is to pass a `password` parameter in the app configuration object which
|
|
457
|
-
will be used to derive the encryption key:
|
|
271
|
+
Standard WebRTC connects every browser directly to every other browser, which is
|
|
272
|
+
ideal for smaller hangouts and matches. Building a 100-person collaborative
|
|
273
|
+
canvas or a crowded party lobby? Opt into the `scale()` plugin:
|
|
458
274
|
|
|
459
275
|
```js
|
|
460
|
-
joinRoom
|
|
461
|
-
```
|
|
276
|
+
import {joinRoom, scale} from 'trystero'
|
|
462
277
|
|
|
463
|
-
|
|
464
|
-
match for all peers in the room for them to be able to connect. An example use
|
|
465
|
-
case might be a private chat room where users learn the password via external
|
|
466
|
-
means.
|
|
467
|
-
|
|
468
|
-
### React hooks
|
|
469
|
-
|
|
470
|
-
Trystero functions are idempotent so they already work out of the box as React
|
|
471
|
-
hooks.
|
|
472
|
-
|
|
473
|
-
Here's a simple example component where each peer syncs their favorite color to
|
|
474
|
-
everyone else:
|
|
475
|
-
|
|
476
|
-
```jsx
|
|
477
|
-
import {joinRoom} from 'trystero'
|
|
478
|
-
import {useState} from 'react'
|
|
479
|
-
|
|
480
|
-
const trysteroConfig = {appId: 'thurn-und-taxis'}
|
|
481
|
-
|
|
482
|
-
export default function App({roomId}) {
|
|
483
|
-
const room = joinRoom(trysteroConfig, roomId)
|
|
484
|
-
const colorAction = room.makeAction('color')
|
|
485
|
-
const [myColor, setMyColor] = useState('#c0ffee')
|
|
486
|
-
const [peerColors, setPeerColors] = useState({})
|
|
487
|
-
|
|
488
|
-
// whenever new peers join the room, send my color to them:
|
|
489
|
-
room.onPeerJoin = peer => colorAction.send(myColor, {target: peer})
|
|
490
|
-
|
|
491
|
-
// listen for peers sending their colors and update the state accordingly:
|
|
492
|
-
colorAction.onMessage = (color, {peerId}) =>
|
|
493
|
-
setPeerColors(peerColors => ({...peerColors, [peerId]: color}))
|
|
494
|
-
|
|
495
|
-
const updateColor = e => {
|
|
496
|
-
const {value} = e.target
|
|
497
|
-
|
|
498
|
-
// when updating my own color, broadcast it to all peers:
|
|
499
|
-
colorAction.send(value)
|
|
500
|
-
setMyColor(value)
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
return (
|
|
504
|
-
<>
|
|
505
|
-
<h1>Trystero + React</h1>
|
|
506
|
-
|
|
507
|
-
<h2>My color:</h2>
|
|
508
|
-
<input type="color" value={myColor} onChange={updateColor} />
|
|
509
|
-
|
|
510
|
-
<h2>Peer colors:</h2>
|
|
511
|
-
<ul>
|
|
512
|
-
{Object.entries(peerColors).map(([peerId, color]) => (
|
|
513
|
-
<li key={peerId} style={{backgroundColor: color}}>
|
|
514
|
-
{peerId}: {color}
|
|
515
|
-
</li>
|
|
516
|
-
))}
|
|
517
|
-
</ul>
|
|
518
|
-
</>
|
|
519
|
-
)
|
|
520
|
-
}
|
|
278
|
+
const room = joinRoom({appId: 'canvas.example.com', use: [scale()]}, 'plaza')
|
|
521
279
|
```
|
|
522
280
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
281
|
+
Small rooms stay in a fast direct mesh with zero extra overhead. As more people
|
|
282
|
+
pile in, Trystero automatically forms a multi-hop gossip network so no single
|
|
283
|
+
browser gets overwhelmed β while your state, action, and media code stays
|
|
284
|
+
exactly the same.
|
|
527
285
|
|
|
528
|
-
|
|
529
|
-
import {joinRoom} from 'trystero'
|
|
530
|
-
import {useEffect, useRef} from 'react'
|
|
531
|
-
|
|
532
|
-
export const useRoom = (roomConfig, roomId) => {
|
|
533
|
-
const roomRef = useRef(joinRoom(roomConfig, roomId))
|
|
534
|
-
const lastRoomIdRef = useRef(roomId)
|
|
535
|
-
|
|
536
|
-
useEffect(() => {
|
|
537
|
-
if (roomId !== lastRoomIdRef.current) {
|
|
538
|
-
roomRef.current.leave()
|
|
539
|
-
roomRef.current = joinRoom(roomConfig, roomId)
|
|
540
|
-
lastRoomIdRef.current = roomId
|
|
541
|
-
}
|
|
286
|
+
π **[Scaling Large Rooms guide β](https://github.com/dmotz/trystero/blob/main/docs/scaling.md)**
|
|
542
287
|
|
|
543
|
-
|
|
544
|
-
}, [roomConfig, roomId])
|
|
288
|
+
---
|
|
545
289
|
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
```
|
|
290
|
+
### 7. Run headless peers in Node, Bun & Deno
|
|
549
291
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
using it. If you find that certain user pairings aren't working in Trystero,
|
|
554
|
-
you're likely encountering an issue at the network provider level. To solve this
|
|
555
|
-
you can configure a TURN server which will act as a proxy layer for peers that
|
|
556
|
-
aren't able to connect directly to one another.
|
|
557
|
-
|
|
558
|
-
1. If you can, confirm that the issue is specific to particular network
|
|
559
|
-
conditions (e.g. user with ISP X cannot connect to a user with ISP Y). If
|
|
560
|
-
other user pairings are working (like those between two browsers on the same
|
|
561
|
-
machine), this likely confirms that Trystero is working correctly.
|
|
562
|
-
2. Sign up for a TURN service or host your own. There are various hosted TURN
|
|
563
|
-
services you can find online like
|
|
564
|
-
[Cloudflare](https://developers.cloudflare.com/calls/turn/) (which offers a
|
|
565
|
-
free tier with 1,000 GB traffic per month) or
|
|
566
|
-
[Open Relay](https://www.metered.ca/stun-turn). You can also host an open
|
|
567
|
-
source TURN server like [coturn](https://github.com/coturn/coturn),
|
|
568
|
-
[Pion TURN](https://github.com/pion/turn),
|
|
569
|
-
[Violet](https://github.com/paullouisageneau/violet), or
|
|
570
|
-
[eturnal](https://github.com/processone/eturnal). Keep in mind data will only
|
|
571
|
-
go through the TURN server for peers that can't directly connect and will
|
|
572
|
-
still be end-to-end encrypted.
|
|
573
|
-
3. Once you have a TURN server, configure Trystero with it like this:
|
|
574
|
-
```js
|
|
575
|
-
const room = joinRoom(
|
|
576
|
-
{
|
|
577
|
-
// ...your app config
|
|
578
|
-
turnConfig: [
|
|
579
|
-
{
|
|
580
|
-
// single string or list of strings of URLs to access TURN server
|
|
581
|
-
urls: ['turn:your-turn-server.ok:1979'],
|
|
582
|
-
username: 'username',
|
|
583
|
-
credential: 'password'
|
|
584
|
-
}
|
|
585
|
-
]
|
|
586
|
-
},
|
|
587
|
-
'roomId'
|
|
588
|
-
)
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
### Running server-side (Node, Bun, Deno)
|
|
592
|
-
|
|
593
|
-
Trystero works outside browsers too, like in Node, Bun, or Deno. Why would you
|
|
594
|
-
want to run something that helps you avoid servers on a server? One reason is if
|
|
595
|
-
you want an always-on peer which can be useful for remembering the last state of
|
|
596
|
-
data, broadcasting it to new users. Another reason might be to run peers that
|
|
597
|
-
are lighter weight and don't need a full browser running, like an embedded
|
|
598
|
-
device or Raspberry Pi.
|
|
599
|
-
|
|
600
|
-
Running server-side uses the same syntax as in the browser, but you need to
|
|
601
|
-
import a polyfill for WebRTC support:
|
|
292
|
+
Trystero runs outside the browser too. Drop a lightweight peer onto a Raspberry
|
|
293
|
+
Pi, build a game bot, or run a silent `passive: true` peer on a tiny box to keep
|
|
294
|
+
shared room state warm even when every browser tab closes:
|
|
602
295
|
|
|
603
296
|
```js
|
|
604
297
|
import {joinRoom} from 'trystero'
|
|
605
298
|
import {RTCPeerConnection} from 'werift'
|
|
606
299
|
|
|
607
300
|
const room = joinRoom(
|
|
608
|
-
{appId: '
|
|
609
|
-
'
|
|
301
|
+
{appId: 'oedipa/synth-jam', rtcPolyfill: RTCPeerConnection, passive: true},
|
|
302
|
+
'room-1'
|
|
610
303
|
)
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
### Self-hosted WebSocket relay
|
|
614
|
-
|
|
615
|
-
If you want a tiny relay that you control, use the WebSocket relay package.
|
|
616
|
-
Start the relay on Node, Bun, or Deno:
|
|
617
|
-
|
|
618
|
-
```js
|
|
619
|
-
import {createWsRelayServer} from '@trystero-p2p/ws-relay/server'
|
|
620
|
-
|
|
621
|
-
createWsRelayServer({port: 8080})
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
Then in the browser:
|
|
625
|
-
|
|
626
|
-
```js
|
|
627
|
-
import {joinRoom} from '@trystero-p2p/ws-relay'
|
|
628
304
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
appId: 'app-id',
|
|
632
|
-
relayConfig: {
|
|
633
|
-
urls: ['wss://localhost:8080']
|
|
634
|
-
}
|
|
635
|
-
},
|
|
636
|
-
'room-id'
|
|
637
|
-
)
|
|
305
|
+
// Silently caches and syncs the latest pattern for newcomers
|
|
306
|
+
room.makeState('pattern', {bpm: 120, steps: []})
|
|
638
307
|
```
|
|
639
308
|
|
|
640
|
-
|
|
641
|
-
are no public default servers. You can pass multiple.
|
|
642
|
-
|
|
643
|
-
### Write your own strategy
|
|
644
|
-
|
|
645
|
-
If you want to provide your own signaling backend, you can build a custom
|
|
646
|
-
strategy with `createTopicStrategy`. This is the recommended helper for simple
|
|
647
|
-
pub/sub relays because Trystero handles room lifecycle details like passive
|
|
648
|
-
mode, announce scheduling, and peer-specific signaling topics.
|
|
649
|
-
|
|
650
|
-
The example below assumes a WebSocket relay that does simple pub/sub routing:
|
|
309
|
+
π **[Running in Node, Bun & Deno guide β](https://github.com/dmotz/trystero/blob/main/docs/server-side.md)**
|
|
651
310
|
|
|
652
|
-
|
|
653
|
-
`{"type": "subscribe" | "unsubscribe" | "publish", "topic", "payload"}`
|
|
654
|
-
- Server broadcasts `{"topic", "payload"}` to subscribers of each topic
|
|
655
|
-
|
|
656
|
-
This is just to show you how it works; if you want a simple self-hosted
|
|
657
|
-
solution, use the `ws-relay` package explained above.
|
|
658
|
-
|
|
659
|
-
```js
|
|
660
|
-
import {createTopicStrategy, toJson} from '@trystero-p2p/core'
|
|
661
|
-
|
|
662
|
-
export const joinRoom = createTopicStrategy({
|
|
663
|
-
// Define init as a function that returns a promise of your signaling client.
|
|
664
|
-
// Resolve the promise when your client is ready to send messages.
|
|
665
|
-
// You can also return an array of client promises for redundancy.
|
|
666
|
-
// In this case, the client is a single WebSocket.
|
|
667
|
-
init: config =>
|
|
668
|
-
new Promise((resolve, reject) => {
|
|
669
|
-
const ws = new WebSocket(config.relayConfig.urls[0])
|
|
670
|
-
ws.addEventListener('open', () => resolve(ws), {once: true})
|
|
671
|
-
ws.addEventListener('error', reject, {once: true})
|
|
672
|
-
}),
|
|
673
|
-
|
|
674
|
-
// Subscribe to one topic. Trystero decides which topics are needed and when.
|
|
675
|
-
subscribeTopic: (client, topic, onMessage) => {
|
|
676
|
-
const onWsMessage = event => {
|
|
677
|
-
const message = JSON.parse(String(event.data))
|
|
678
|
-
|
|
679
|
-
if (message.topic !== topic) {
|
|
680
|
-
return
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
onMessage(message.topic, message.payload)
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
client.addEventListener('message', onWsMessage)
|
|
687
|
-
client.send(toJson({type: 'subscribe', topic}))
|
|
688
|
-
|
|
689
|
-
return () => {
|
|
690
|
-
client.send(toJson({type: 'unsubscribe', topic}))
|
|
691
|
-
client.removeEventListener('message', onWsMessage)
|
|
692
|
-
}
|
|
693
|
-
},
|
|
694
|
-
|
|
695
|
-
// Publish a payload to one topic.
|
|
696
|
-
publishTopic: (client, topic, payload) =>
|
|
697
|
-
client.send(
|
|
698
|
-
toJson({
|
|
699
|
-
type: 'publish',
|
|
700
|
-
topic,
|
|
701
|
-
payload
|
|
702
|
-
})
|
|
703
|
-
)
|
|
704
|
-
})
|
|
705
|
-
|
|
706
|
-
const room = joinRoom(
|
|
707
|
-
{appId: 'my-app-id', relayConfig: {urls: ['wss://my-relay.example']}},
|
|
708
|
-
'my-room-id'
|
|
709
|
-
)
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
`createTopicStrategy` uses a short startup announcement burst followed by a
|
|
713
|
-
60-second steady interval and repeats the burst after a peer drops. Set
|
|
714
|
-
`reannounceOnDisconnect: false` when the signaling backend stores peer presence
|
|
715
|
-
rather than delivering announcements as transient events.
|
|
716
|
-
|
|
717
|
-
For non-pub/sub signaling protocols, such as trackers that exchange offers in
|
|
718
|
-
bulk, `createStrategy` is available as a lower-level advanced API.
|
|
719
|
-
|
|
720
|
-
### Supabase setup
|
|
721
|
-
|
|
722
|
-
To use the Supabase strategy:
|
|
311
|
+
---
|
|
723
312
|
|
|
724
|
-
|
|
725
|
-
2. On the dashboard, go to Project Settings -> API
|
|
726
|
-
3. Copy the Project URL and set that as the `appId` in the Trystero config, copy
|
|
727
|
-
the `anon public` API key and set it as `relayConfig.supabaseKey` in the
|
|
728
|
-
Trystero config
|
|
313
|
+
## How does Trystero connect users with no backend?
|
|
729
314
|
|
|
730
|
-
|
|
315
|
+
WebRTC lets browsers talk directly to each other, peer-to-peer, without routing
|
|
316
|
+
traffic through a central server. The one catch with WebRTC has always been
|
|
317
|
+
_signaling_: two browsers need a brief introduction to swap connection info
|
|
318
|
+
before they can link up.
|
|
731
319
|
|
|
732
|
-
|
|
320
|
+
Trystero handles that introduction automatically out of the box using
|
|
321
|
+
decentralized public relays β nothing to deploy or configure. Once peers
|
|
322
|
+
connect, **your app's data never touches a relay**: state, messages, files, and
|
|
323
|
+
media flow straight from browser to browser, end-to-end encrypted.
|
|
733
324
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
325
|
+
Prefer to run your own signaling setup with Nostr, MQTT, BitTorrent, Supabase,
|
|
326
|
+
Firebase, or a tiny self-hosted WebSocket relay? You can swap strategies with a
|
|
327
|
+
one-line import change β see the
|
|
328
|
+
**[Signaling & Relays guide](https://github.com/dmotz/trystero/blob/main/docs/signaling.md)**.
|
|
737
329
|
|
|
738
330
|
<details>
|
|
739
|
-
<summary>
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
These rules ensure room peer presence is only readable if the room namespace is
|
|
761
|
-
known ahead of time.
|
|
331
|
+
<summary>Peek under the hood: how serverless discovery and security work</summary>
|
|
332
|
+
|
|
333
|
+
- **Encrypted signaling**: Connection offers (SDPs) are encrypted with
|
|
334
|
+
AES-256-GCM before touching any relay. Adding a room `password` derives the
|
|
335
|
+
key via WebCrypto `PBKDF2-SHA256` (600,000 iterations) so outsiders and relay
|
|
336
|
+
operators can't read handshakes or join the room.
|
|
337
|
+
- **Self-certifying identities**: Every user (`userId`) and browser tab
|
|
338
|
+
(`peerId`) is derived from a WebCrypto ECDSA P-256 public keypair. During
|
|
339
|
+
connection setup, peers verify each other's keys against the WebRTC DTLS
|
|
340
|
+
certificate fingerprint
|
|
341
|
+
([RFC 8827](https://www.rfc-editor.org/rfc/rfc8827.html)), preventing spoofing
|
|
342
|
+
and man-in-the-middle relays.
|
|
343
|
+
- **Signed state & roles**: Updates to `makeUserState`, `makeState`, and
|
|
344
|
+
`roles()` carry cryptographic signatures and Hybrid Logical Clock timestamps,
|
|
345
|
+
allowing peers (including late joiners) to verify who wrote what without a
|
|
346
|
+
central server.
|
|
762
347
|
|
|
763
348
|
</details>
|
|
764
349
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
### `joinRoom(config, roomId, [callbacks])`
|
|
768
|
-
|
|
769
|
-
Adds local user to room whereby other peers in the same namespace will open
|
|
770
|
-
communication channels and send events. Calling `joinRoom()` multiple times with
|
|
771
|
-
the same namespace will return the same room instance.
|
|
772
|
-
|
|
773
|
-
- `config` - Configuration object containing the following keys:
|
|
774
|
-
- `appId` - **(required)** A unique string identifying your app. When using
|
|
775
|
-
Supabase, this should be set to your project URL (see
|
|
776
|
-
[Supabase setup instructions](#supabase-setup)). If using Firebase, this
|
|
777
|
-
should be the `databaseURL` from your Firebase config (also see
|
|
778
|
-
`relayConfig.firebaseApp` below for an alternative way of configuring the
|
|
779
|
-
Firebase strategy).
|
|
780
|
-
|
|
781
|
-
- `password` - **(optional)** A string to encrypt session descriptions via
|
|
782
|
-
AES-GCM as they are passed through the peering medium. If not set, session
|
|
783
|
-
descriptions will be encrypted with a key derived from the app ID and room
|
|
784
|
-
name. A custom password must match between any peers in the room for them to
|
|
785
|
-
connect. See [encryption](#encryption) for more details.
|
|
786
|
-
|
|
787
|
-
- `maxReceiveBytes` - **(optional)** Maximum payload size and budget for
|
|
788
|
-
concurrent large incoming transfers per room, in bytes. Defaults to
|
|
789
|
-
`256 * 1024 ** 2` (256 MiB). Must be a positive integer. Raise it for larger
|
|
790
|
-
files or lower it to reduce memory use.
|
|
791
|
-
|
|
792
|
-
- `passive` - **(optional)** Boolean for backup or relay peers that should
|
|
793
|
-
listen for active peers without announcing themselves while a room is
|
|
794
|
-
dormant. Passive peers activate only after hearing a non-passive peer and
|
|
795
|
-
include a passive flag in their signaling so passive peers do not connect to
|
|
796
|
-
each other. For BitTorrent, dormant passive rooms announce as seeders
|
|
797
|
-
(`left: 0`) without offers, which avoids passive-to-passive discovery while
|
|
798
|
-
keeping tracker load low.
|
|
799
|
-
|
|
800
|
-
- `relayConfig` - **(optional unless required by your chosen strategy)**
|
|
801
|
-
Object containing strategy-specific relay settings:
|
|
802
|
-
- `urls` - **(optional for π BitTorrent, π¦ Nostr, π‘ MQTT; required for π
|
|
803
|
-
WebSocket relay)** Custom list of URLs for the strategy to use to
|
|
804
|
-
bootstrap P2P connections. These would be BitTorrent trackers, Nostr
|
|
805
|
-
relays, MQTT brokers, and WebSocket relays, respectively. They must
|
|
806
|
-
support secure WebSocket connections.
|
|
807
|
-
|
|
808
|
-
- `redundancy` - **(optional, π BitTorrent, π¦ Nostr, π‘ MQTT only)**
|
|
809
|
-
Integer specifying how many default relay endpoints to connect to
|
|
810
|
-
simultaneously. Passing a `urls` option will cause this option to be
|
|
811
|
-
ignored as the entire list will be used.
|
|
812
|
-
|
|
813
|
-
- `manualReconnection` - **(optional, π¦ Nostr and π BitTorrent only)**
|
|
814
|
-
Boolean (default: `false`) that when set to `true` disables automatically
|
|
815
|
-
pausing and resuming reconnection attempts when the browser goes offline
|
|
816
|
-
and comes back online. This is useful if you want to manage this behavior
|
|
817
|
-
yourself.
|
|
818
|
-
|
|
819
|
-
- `warnOnRelayFailure` - **(optional)** Boolean (default: `true`) that when
|
|
820
|
-
set to `false` suppresses warnings caused by relay failures. This includes
|
|
821
|
-
failed announce attempts and strategy-specific failure messages from
|
|
822
|
-
relays or trackers.
|
|
823
|
-
|
|
824
|
-
- `supabaseKey` - **(required, β‘οΈ Supabase only)** Your Supabase project's
|
|
825
|
-
`anon public` API key.
|
|
826
|
-
|
|
827
|
-
- `firebaseApp` - **(optional, π₯ Firebase only)** You can pass an already
|
|
828
|
-
initialized Firebase app instance instead of an `appId`. Normally Trystero
|
|
829
|
-
will initialize a Firebase app based on the `appId` but this will fail if
|
|
830
|
-
you've already initialized it for use elsewhere.
|
|
831
|
-
|
|
832
|
-
- `firebasePath` - **(optional, π₯ Firebase only)** String specifying path
|
|
833
|
-
where Trystero writes its matchmaking data in your database
|
|
834
|
-
(`'__trystero__'` by default). Changing this is useful if you want to run
|
|
835
|
-
multiple apps using the same database and don't want to worry about
|
|
836
|
-
namespace collisions.
|
|
837
|
-
|
|
838
|
-
- `rtcConfig` - **(optional)** Specifies a custom
|
|
839
|
-
[`RTCConfiguration`](https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration)
|
|
840
|
-
for all peer connections.
|
|
841
|
-
|
|
842
|
-
- `trickleIce` - **(optional)** Boolean controlling whether ICE candidates are
|
|
843
|
-
sent incrementally (`true`) or bundled with SDP (`false`). Default is
|
|
844
|
-
strategy-dependent: `true` for most strategies, `false` for BitTorrent and
|
|
845
|
-
IPFS unless explicitly set.
|
|
846
|
-
|
|
847
|
-
- `turnConfig` - **(optional)** Specifies a custom list of TURN servers to use
|
|
848
|
-
(see
|
|
849
|
-
[Troubleshooting connection issues](#troubleshooting-connection-issues)).
|
|
850
|
-
Each item in the list should correspond to an
|
|
851
|
-
[ICE server config object](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers).
|
|
852
|
-
When passing a TURN config like this, Trystero's default STUN servers will
|
|
853
|
-
also be used. To override this and use both custom STUN and TURN servers,
|
|
854
|
-
instead pass the config via the above `rtcConfig.iceServers` option as a
|
|
855
|
-
list of both STUN/TURN servers β this won't inherit Trystero's defaults.
|
|
856
|
-
|
|
857
|
-
- `rtcPolyfill` - **(optional)** Use this to pass a custom
|
|
858
|
-
[`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection)-compatible
|
|
859
|
-
constructor. This is useful for running outside of a browser, such as in
|
|
860
|
-
Node (still experimental).
|
|
861
|
-
|
|
862
|
-
- `roomId` - A string to namespace peers and events within a room.
|
|
863
|
-
|
|
864
|
-
- `callbacks` - **(optional)** Callback config object containing:
|
|
865
|
-
- `onJoinError(details)` - Called when room join fails due to an incorrect
|
|
866
|
-
password, when handshake admission fails (including timeout), or when peers
|
|
867
|
-
exchange SDP but WebRTC still cannot establish a direct connection. This
|
|
868
|
-
last case usually means TURN servers are needed or misconfigured (see
|
|
869
|
-
[Troubleshooting connection issues](#troubleshooting-connection-issues)).
|
|
870
|
-
`details` is an object containing `appId`, `roomId`, `peerId`, and `error`
|
|
871
|
-
describing the failure.
|
|
872
|
-
|
|
873
|
-
- `onPeerHandshake(peerId, send, receive, isInitiator)` - Async predicate that
|
|
874
|
-
runs after the transport connects but before the peer becomes active.
|
|
875
|
-
Return/resolve to accept the peer, throw/reject to deny the peer.
|
|
876
|
-
- `peerId` - ID of the pending peer.
|
|
877
|
-
- `send(data, [metadata])` - Sends handshake payloads to the pending peer.
|
|
878
|
-
- `receive()` - Resolves to the next handshake message from the peer as
|
|
879
|
-
`{data, metadata}`.
|
|
880
|
-
- `isInitiator` - Deterministic role flag for avoiding protocol deadlocks.
|
|
881
|
-
|
|
882
|
-
- `handshakeTimeoutMs` - Timeout for pending handshakes in milliseconds
|
|
883
|
-
(`10000` by default). If exceeded, the peer is denied and `onJoinError` is
|
|
884
|
-
called.
|
|
885
|
-
|
|
886
|
-
During handshake, the peer remains pending and is not included in `getPeers()`
|
|
887
|
-
and does not trigger Trystero API events (`onPeerJoin`, action receivers,
|
|
888
|
-
stream/track callbacks). Non-handshake data received while pending is dropped.
|
|
889
|
-
|
|
890
|
-
Minimal handshake example:
|
|
891
|
-
|
|
892
|
-
```js
|
|
893
|
-
import {joinRoom} from 'trystero'
|
|
894
|
-
|
|
895
|
-
const room = joinRoom({appId: 'my-app'}, 'secure-room', {
|
|
896
|
-
onPeerHandshake: async (_, send, receive, isInitiator) => {
|
|
897
|
-
if (isInitiator) {
|
|
898
|
-
await send({challenge: 'prove-you-know-the-secret'})
|
|
899
|
-
const {data} = await receive()
|
|
900
|
-
|
|
901
|
-
if (data?.response !== 'shared-secret') {
|
|
902
|
-
throw new Error('handshake rejected')
|
|
903
|
-
}
|
|
904
|
-
} else {
|
|
905
|
-
const {data} = await receive()
|
|
906
|
-
|
|
907
|
-
if (data?.challenge !== 'prove-you-know-the-secret') {
|
|
908
|
-
throw new Error('handshake rejected')
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
await send({response: 'shared-secret'})
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
})
|
|
915
|
-
```
|
|
916
|
-
|
|
917
|
-
Returns an object with the following methods:
|
|
918
|
-
|
|
919
|
-
- ### `leave()`
|
|
920
|
-
|
|
921
|
-
Remove local user from room and unsubscribe from room events.
|
|
922
|
-
|
|
923
|
-
- ### `getPeers()`
|
|
924
|
-
|
|
925
|
-
Returns a map of
|
|
926
|
-
[`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection)s
|
|
927
|
-
for the peers present in room (not including the local user). The keys of this
|
|
928
|
-
object are the respective peers' IDs.
|
|
929
|
-
|
|
930
|
-
- ### `isPassive()`
|
|
931
|
-
|
|
932
|
-
Returns whether the room was joined with `passive: true`.
|
|
933
|
-
|
|
934
|
-
- ### `addStream(stream, [options])`
|
|
935
|
-
|
|
936
|
-
Broadcasts media stream to other peers.
|
|
937
|
-
- `stream` - A `MediaStream` with audio and/or video to send to peers in the
|
|
938
|
-
room.
|
|
939
|
-
|
|
940
|
-
- `options.target` - **(optional)** If specified, the stream is sent only to
|
|
941
|
-
the target peer ID (string) or list of peer IDs (array). Passing `null` or
|
|
942
|
-
omitting this option sends to all peers in the room.
|
|
943
|
-
|
|
944
|
-
- `options.metadata` - **(optional)** Additional metadata (any serializable
|
|
945
|
-
type) to be sent with the stream. This is useful when sending multiple
|
|
946
|
-
streams so recipients know which is which (e.g. a webcam versus a screen
|
|
947
|
-
capture).
|
|
948
|
-
|
|
949
|
-
- ### `removeStream(stream, [options])`
|
|
950
|
-
|
|
951
|
-
Stops sending previously sent media stream to other peers.
|
|
952
|
-
- `stream` - A previously sent `MediaStream` to stop sending.
|
|
953
|
-
|
|
954
|
-
- `options.target` - **(optional)** If specified, the stream is removed only
|
|
955
|
-
from the target peer ID (string) or list of peer IDs (array).
|
|
956
|
-
|
|
957
|
-
- ### `addTrack(track, stream, [options])`
|
|
958
|
-
|
|
959
|
-
Adds a new media track to a stream.
|
|
960
|
-
- `track` - A `MediaStreamTrack` to add to an existing stream.
|
|
961
|
-
|
|
962
|
-
- `stream` - The target `MediaStream` to attach the new track to.
|
|
963
|
-
|
|
964
|
-
- `options.target` - **(optional)** If specified, the track is sent only to
|
|
965
|
-
the target peer ID (string) or list of peer IDs (array).
|
|
966
|
-
|
|
967
|
-
- `options.metadata` - **(optional)** Additional metadata (any serializable
|
|
968
|
-
type) to be sent with the track. See `metadata` notes for `addStream()`
|
|
969
|
-
above for more details.
|
|
970
|
-
|
|
971
|
-
- ### `removeTrack(track, [options])`
|
|
972
|
-
|
|
973
|
-
Removes a media track.
|
|
974
|
-
- `track` - The `MediaStreamTrack` to remove.
|
|
975
|
-
|
|
976
|
-
- `options.target` - **(optional)** If specified, the track is removed only
|
|
977
|
-
from the target peer ID (string) or list of peer IDs (array).
|
|
978
|
-
|
|
979
|
-
- ### `replaceTrack(oldTrack, newTrack, [options])`
|
|
980
|
-
|
|
981
|
-
Replaces a media track with a new one.
|
|
982
|
-
- `oldTrack` - The `MediaStreamTrack` to remove.
|
|
983
|
-
|
|
984
|
-
- `newTrack` - A `MediaStreamTrack` to attach.
|
|
985
|
-
|
|
986
|
-
- `options.target` - **(optional)** If specified, the track is replaced only
|
|
987
|
-
for the target peer ID (string) or list of peer IDs (array).
|
|
988
|
-
|
|
989
|
-
- `options.metadata` - **(optional)** Additional metadata (any serializable
|
|
990
|
-
type) to be sent with the replacement track.
|
|
991
|
-
|
|
992
|
-
- ### `onPeerJoin`
|
|
993
|
-
|
|
994
|
-
A callback property that will be called when a peer joins the room. Assigning
|
|
995
|
-
a new function replaces the previous handler; assigning `null` clears it.
|
|
996
|
-
Existing active peers are immediately replayed to a newly assigned handler.
|
|
997
|
-
- `callback(peerId)` - Function to run whenever a peer joins, called with the
|
|
998
|
-
peer's ID.
|
|
999
|
-
|
|
1000
|
-
Example:
|
|
1001
|
-
|
|
1002
|
-
```js
|
|
1003
|
-
room.onPeerJoin = peerId => console.log(`${peerId} joined`)
|
|
1004
|
-
```
|
|
1005
|
-
|
|
1006
|
-
- ### `onPeerLeave`
|
|
1007
|
-
|
|
1008
|
-
A callback property that will be called when a peer leaves the room. Assigning
|
|
1009
|
-
a new function replaces the previous handler; assigning `null` clears it.
|
|
1010
|
-
- `callback(peerId)` - Function to run whenever a peer leaves, called with the
|
|
1011
|
-
peer's ID.
|
|
1012
|
-
|
|
1013
|
-
Example:
|
|
1014
|
-
|
|
1015
|
-
```js
|
|
1016
|
-
room.onPeerLeave = peerId => console.log(`${peerId} left`)
|
|
1017
|
-
```
|
|
1018
|
-
|
|
1019
|
-
- ### `onPeerStream`
|
|
1020
|
-
|
|
1021
|
-
A callback property that will be called when a peer sends a media stream.
|
|
1022
|
-
Assigning a new function replaces the previous handler; assigning `null`
|
|
1023
|
-
clears it.
|
|
1024
|
-
- `callback(stream, peerId, metadata)` - Function to run whenever a peer sends
|
|
1025
|
-
a media stream, called with the the peer's stream, ID, and optional metadata
|
|
1026
|
-
(see `addStream()` above for details).
|
|
1027
|
-
|
|
1028
|
-
Example:
|
|
1029
|
-
|
|
1030
|
-
```js
|
|
1031
|
-
room.onPeerStream = (stream, peerId) =>
|
|
1032
|
-
console.log(`got stream from ${peerId}`, stream)
|
|
1033
|
-
```
|
|
1034
|
-
|
|
1035
|
-
- ### `onPeerTrack`
|
|
1036
|
-
|
|
1037
|
-
A callback property that will be called when a peer sends a media track.
|
|
1038
|
-
Assigning a new function replaces the previous handler; assigning `null`
|
|
1039
|
-
clears it.
|
|
1040
|
-
- `callback(track, stream, peerId, metadata)` - Function to run whenever a
|
|
1041
|
-
peer sends a media track, called with the the peer's track, attached stream,
|
|
1042
|
-
ID, and optional metadata (see `addTrack()` above for details).
|
|
1043
|
-
|
|
1044
|
-
Example:
|
|
1045
|
-
|
|
1046
|
-
```js
|
|
1047
|
-
room.onPeerTrack = (track, stream, peerId) =>
|
|
1048
|
-
console.log(`got track from ${peerId}`, track)
|
|
1049
|
-
```
|
|
1050
|
-
|
|
1051
|
-
- ### `makeAction(actionId, [config])`
|
|
1052
|
-
|
|
1053
|
-
Listen for and send custom data actions.
|
|
1054
|
-
- `actionId` - A string to register this action consistently among all peers.
|
|
1055
|
-
|
|
1056
|
-
If `config.kind` is omitted, the action is a one-way message action. Passing
|
|
1057
|
-
`kind: 'request'` creates a request/response action.
|
|
1058
|
-
|
|
1059
|
-
Message actions expose:
|
|
1060
|
-
- `send(data, [options])` - Sends data to peers and resolves when local
|
|
1061
|
-
sending is complete. To cancel an in-progress send, pass an
|
|
1062
|
-
`AbortController`'s `signal` in the options (e.g.
|
|
1063
|
-
`send(data, {signal: controller.signal})`) and call `controller.abort()`.
|
|
1064
|
-
- `onMessage` - Nullable callback property for received messages.
|
|
1065
|
-
- `onReceiveProgress` - Nullable callback property for inbound progress.
|
|
1066
|
-
|
|
1067
|
-
Request actions expose:
|
|
1068
|
-
- `request(data, options)` - Sends to one peer and resolves with its response.
|
|
1069
|
-
- `requestMany(data, options)` - Sends to many peers and resolves with
|
|
1070
|
-
peer-labeled settled results.
|
|
1071
|
-
- `onRequest` - Nullable callback property that returns the response.
|
|
1072
|
-
- `onReceiveProgress` - Nullable callback property for inbound progress.
|
|
1073
|
-
|
|
1074
|
-
Both kinds expose `onReceive`, a nullable callback accepting
|
|
1075
|
-
`{byteLength, peerId, metadata, kind, signal}` and returning a boolean or a
|
|
1076
|
-
promise of a boolean. It can also be provided in `config`. `kind` is
|
|
1077
|
-
`'message'`, `'request'`, or `'response'`; request actions apply it to both
|
|
1078
|
-
requests and responses. Responses have no metadata. See
|
|
1079
|
-
[receiving files](#receiving-files).
|
|
1080
|
-
|
|
1081
|
-
Send options use `target`, `metadata`, `onProgress`, and `signal`. Request
|
|
1082
|
-
options use `target`, `metadata`, `timeoutMs`, `onProgress`, and `signal`.
|
|
1083
|
-
`requestMany()` uses `targets` plus optional `onResult` for each peer result
|
|
1084
|
-
as it arrives. Request timeouts cover approval, sending, and the response.
|
|
1085
|
-
|
|
1086
|
-
Example:
|
|
1087
|
-
|
|
1088
|
-
```js
|
|
1089
|
-
const cursor = room.makeAction('cursormove')
|
|
1090
|
-
|
|
1091
|
-
window.addEventListener('mousemove', e => cursor.send([e.clientX, e.clientY]))
|
|
1092
|
-
|
|
1093
|
-
cursor.onMessage = ([x, y], {peerId}) => {
|
|
1094
|
-
const peerCursor = cursorMap[peerId]
|
|
1095
|
-
peerCursor.style.left = x + 'px'
|
|
1096
|
-
peerCursor.style.top = y + 'px'
|
|
1097
|
-
}
|
|
1098
|
-
```
|
|
1099
|
-
|
|
1100
|
-
- ### `ping(peerId)`
|
|
1101
|
-
|
|
1102
|
-
Takes a peer ID and returns a promise that resolves to the milliseconds the
|
|
1103
|
-
round-trip to that peer took. Use this for measuring latency.
|
|
1104
|
-
- `peerId` - Peer ID string of the target peer.
|
|
1105
|
-
|
|
1106
|
-
Example:
|
|
1107
|
-
|
|
1108
|
-
```js
|
|
1109
|
-
// log round-trip time every 2 seconds
|
|
1110
|
-
room.onPeerJoin = peerId =>
|
|
1111
|
-
setInterval(
|
|
1112
|
-
async () => console.log(`took ${await room.ping(peerId)}ms`),
|
|
1113
|
-
2000
|
|
1114
|
-
)
|
|
1115
|
-
```
|
|
1116
|
-
|
|
1117
|
-
### `selfId`
|
|
1118
|
-
|
|
1119
|
-
A unique ID string other peers will know the local user as globally across
|
|
1120
|
-
rooms.
|
|
1121
|
-
|
|
1122
|
-
### `getRelaySockets()`
|
|
1123
|
-
|
|
1124
|
-
**(π BitTorrent, π¦ Nostr, π‘ MQTT, π WebSocket relay only)** Returns an
|
|
1125
|
-
object of relay URL keys mapped to their WebSocket connections. This can be
|
|
1126
|
-
useful for determining the state of the user's connection to the relays and
|
|
1127
|
-
handling any connection failures.
|
|
1128
|
-
|
|
1129
|
-
Example:
|
|
1130
|
-
|
|
1131
|
-
```js
|
|
1132
|
-
import {getRelaySockets} from '@trystero-p2p/torrent'
|
|
1133
|
-
|
|
1134
|
-
console.log(getRelaySockets())
|
|
1135
|
-
// => Object {
|
|
1136
|
-
// "wss://tracker.webtorrent.dev": WebSocket,
|
|
1137
|
-
// "wss://tracker.openwebtorrent.com": WebSocket
|
|
1138
|
-
// }
|
|
1139
|
-
```
|
|
1140
|
-
|
|
1141
|
-
### `pauseRelayReconnection()`
|
|
1142
|
-
|
|
1143
|
-
**(π¦ Nostr, π BitTorrent only)** Normally Trystero will try to automatically
|
|
1144
|
-
reconnect to relay sockets unless `relayConfig.manualReconnection: true` is set
|
|
1145
|
-
in the room config. Calling this function stops relay reconnection attempts
|
|
1146
|
-
until `resumeRelayReconnection()` is called.
|
|
1147
|
-
|
|
1148
|
-
### `resumeRelayReconnection()`
|
|
1149
|
-
|
|
1150
|
-
**(π¦ Nostr, π BitTorrent only)** Allows relay reconnection attempts to resume.
|
|
1151
|
-
(See `pauseRelayReconnection()` above).
|
|
1152
|
-
|
|
1153
|
-
## Which strategy should I choose?
|
|
1154
|
-
|
|
1155
|
-
By default Trystero uses the Nostr network which is highly decentralized with
|
|
1156
|
-
hundreds of active relays running. This is a good choice if you're interested in
|
|
1157
|
-
decentralization and high redundancy. The other decentralized strategies are
|
|
1158
|
-
recommended in the order of MQTT, BitTorrent, and IPFS, based on robustness.
|
|
1159
|
-
These networks have far less relay redundancy than Nostr, but you might prefer
|
|
1160
|
-
them for other reasons. You can of course host your own relay server for any of
|
|
1161
|
-
these strategies.
|
|
350
|
+
---
|
|
1162
351
|
|
|
1163
|
-
|
|
1164
|
-
|
|
352
|
+
## Documentation
|
|
353
|
+
|
|
354
|
+
Dive deeper into any feature or browse the full API:
|
|
355
|
+
|
|
356
|
+
| Guide | What's inside |
|
|
357
|
+
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
358
|
+
| **[Shared & Per-User State](https://github.com/dmotz/trystero/blob/main/docs/state.md)** | `makeUserState`, `makeState`, validation rules, late-joiner sync, and React (`useSyncExternalStore`) |
|
|
359
|
+
| **[Identity, Presence & Room Access](https://github.com/dmotz/trystero/blob/main/docs/identity.md)** | `userId` vs `peerId`, multi-tab presence, custom identity keys, room `password`s, `maxUsers`, and `onError` |
|
|
360
|
+
| **[Admins, Roles & Moderation](https://github.com/dmotz/trystero/blob/main/docs/roles.md)** | `roles()`, self-certifying rooms, fixed admins, custom roles, authoritative state, and banning |
|
|
361
|
+
| **[Actions, RPC & File Transfers](https://github.com/dmotz/trystero/blob/main/docs/actions.md)** | `makeAction`, `send`, `request` & `requestMany`, TypeScript types, binary metadata, progress bars, and `onReceive` |
|
|
362
|
+
| **[Audio, Video & Screen Sharing](https://github.com/dmotz/trystero/blob/main/docs/media.md)** | `addStream`, `onPeerStream`, `removeStream`, camera/screen switching (`replaceTrack`), and stream metadata |
|
|
363
|
+
| **[Scaling Large Rooms](https://github.com/dmotz/trystero/blob/main/docs/scaling.md)** | Multi-hop routing with `scale()`, direct media/file promotion, and `getTopology()` |
|
|
364
|
+
| **[Signaling & Relays](https://github.com/dmotz/trystero/blob/main/docs/signaling.md)** | Default zero-config relays, Supabase, Firebase, MQTT, BitTorrent, self-hosted `ws-relay`, custom strategies, and bundle sizes |
|
|
365
|
+
| **[Running in Node, Bun & Deno](https://github.com/dmotz/trystero/blob/main/docs/server-side.md)** | Headless peers, Raspberry Pi / IoT setups, and `passive` state backup peers |
|
|
366
|
+
| **[NAT Traversal & Troubleshooting](https://github.com/dmotz/trystero/blob/main/docs/troubleshooting.md)** | Configuring TURN servers, diagnosing strict firewalls, and `ping()` |
|
|
367
|
+
| **[Full API Reference](https://github.com/dmotz/trystero/blob/main/docs/api.md)** | Complete function signatures, TypeScript types, config options, and error codes |
|
|
368
|
+
| **[Upgrading to 0.27.0](https://github.com/dmotz/trystero/blob/main/MIGRATION.md)** | Architectural overview, breaking changes, and before/after migration examples |
|
|
1165
369
|
|
|
1166
370
|
---
|
|
1167
371
|
|