Build instant multiplayer web apps, no server required
π Try it live on trystero.dev π
Trystero makes browsers discover each other and communicate directly. No accounts. No deploying infrastructure. Just import and connect.
Peers can connect via π BitTorrent, π¦ Nostr, π‘ MQTT, β‘οΈ Supabase, π₯Firebase, πͺ IPFS, or a π self-hosted WebSocket relay β all using the same API.
Besides making peer matching automatic, Trystero offers some nice abstractions on top of WebRTC:
- ππ£ Rooms / broadcasting
- π’π© Automatic serialization / deserialization of data
- π₯π· Attach metadata to binary data and media streams
- βοΈβ³ Automatic chunking and throttling of large data
- β±π€ Progress events and promises for data transfers
- ππ Session data encryption
- πβ‘ Can run peers server-side on Node and Bun
- βοΈπͺ React hooks
You can see what people are building with Trystero here.
- How it works
- Get started
- Listen for events
- Broadcast events
- Audio and video
- Advanced
- API
- Which strategy should I choose?
π If you just want to try out Trystero, you can skip this explainer and jump into using it.
To establish a direct peer-to-peer connection with WebRTC, a signalling channel is needed to exchange peer information (SDP). Typically this involves running your own matchmaking server but Trystero abstracts this away for you and offers multiple strategies for connecting peers (currently BitTorrent, Nostr, MQTT, Supabase, Firebase, IPFS, and self-hosted WebSocket relay).
The important point to remember is this:
π
Beyond peer discovery, your app's data never touches the strategy medium and is sent directly peer-to-peer and end-to-end encrypted between users.
π
You can compare strategies here.
Install Trystero with your preferred package manager, then import it in your code:
npm i trysteroimport {joinRoom} from 'trystero'No package manager? You can also use a CDN:
<script type="module">
import {joinRoom} from 'https://esm.run/trystero'
</script>The default Trystero package runs on the Nostr network, but you can swap in any other stategy by changing which package you import:
import {joinRoom} from '@trystero-p2p/mqtt'
// or
import {joinRoom} from '@trystero-p2p/torrent'
// or
import {joinRoom} from '@trystero-p2p/supabase'
// or
import {joinRoom} from '@trystero-p2p/firebase'
// or
import {joinRoom} from '@trystero-p2p/ipfs'
// or
import {joinRoom} from '@trystero-p2p/ws-relay'Next, join the user to a room with an ID:
const config = {appId: 'san_narciso_3d'}
const room = joinRoom(config, 'yoyodyne')The first argument is a configuration object that requires an appId. This
should be a completely unique identifier for your appΒΉ. The second argument is
the room ID.
Why rooms? Browsers can only handle a limited amount of WebRTC connections at a time so it's recommended to design your app such that users are divided into groups (or rooms, or namespaces, or channels... whatever you'd like to call them).
ΒΉ When using Firebase, appId should be your databaseURL and when using
Supabase, it should be your project URL.
Listen for peers joining the room:
room.onPeerJoin = peerId => console.log(`${peerId} joined`)Listen for peers leaving the room:
room.onPeerLeave = peerId => console.log(`${peerId} left`)Listen for peers sending their audio/video streams:
room.onPeerStream = (stream, peerId) =>
(peerElements[peerId].video.srcObject = stream)To unsubscribe from events, leave the room:
room.leave()You can access the local user's peer ID by importing selfId like so:
import {selfId} from 'trystero'
console.log(`my peer ID is ${selfId}`)Send peers your video stream:
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
})
room.addStream(stream)Send and subscribe to custom peer-to-peer actions:
const drink = room.makeAction('drink')
// buy drink for a friend
drink.send({drink: 'negroni', withIce: true}, {target: friendId})
// buy round for the house
drink.send({drink: 'mezcal', withIce: false})
// listen for drinks sent to you
drink.onMessage = (data, {peerId}) =>
console.log(
`got a ${data.drink} with${data.withIce ? '' : 'out'} ice from ${peerId}`
)Actions can also use request/response semantics:
const isEven = room.makeAction('is-even', {
kind: 'request',
onRequest: n => n % 2 === 0
})
const result = await isEven.request(42, {
target: friendId,
timeoutMs: 1000
})To ask multiple peers at once, use requestMany(). It resolves with a
peer-labeled result for every target, while onResult lets you react as each
peer answers:
const availability = room.makeAction('availability', {
kind: 'request',
onRequest: ({date}) => calendar.isFree(date)
})
const results = await availability.requestMany(
{date: '2026-05-04'},
{
targets: teammateIds,
timeoutMs: 1000,
onResult: result => {
if (result.status === 'fulfilled') {
updateAvailabilityBadge(result.peerId, result.value)
}
}
}
)
const freePeers = results
.filter(result => result.status === 'fulfilled' && result.value)
.map(result => result.peerId)If you're using TypeScript, you can add a type hint to the action:
type CursorMove = {x: number; y: number}
const cursor = room.makeAction<CursorMove>('cursor-move')You can also use actions to send binary data, like images:
const pic = room.makeAction('pic')
// blobs are automatically handled, as are any form of TypedArray
canvas.toBlob(blob => pic.send(blob))
// binary data is received as raw ArrayBuffers so your handling code should
// interpret it in a way that makes sense
pic.onMessage = (data, {peerId}) =>
(imgs[peerId].src = URL.createObjectURL(new Blob([data])))Let's say we want users to be able to name themselves:
const idsToNames = {}
const name = room.makeAction('name')
// tell new peers your name when they connect
room.onPeerJoin = peerId => name.send('Oedipa', {target: peerId})
// listen for peers naming themselves
name.onMessage = (value, {peerId}) => (idsToNames[peerId] = value)
// tell all peers at once when your name changes
nameInput.addEventListener('change', e => name.send(e.target.value))
room.onPeerLeave = peerId =>
console.log(`${idsToNames[peerId] || 'a weird stranger'} left`)Actions are smart and handle serialization and chunking for you behind the scenes. This means you can send very large files and whatever data you send will be received on the other side as the same type (a number as a number, a string as a string, an object as an object, binary as binary, etc.).
Here's a simple example of how you could create an audio chatroom:
// this object can store audio instances for later
const peerAudios = {}
// get a local audio stream from the microphone
const selfStream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: false
})
// send stream to peers currently in the room
room.addStream(selfStream)
// send stream to peers who join later
room.onPeerJoin = peerId => room.addStream(selfStream, {target: peerId})
// handle streams from other peers
room.onPeerStream = (stream, peerId) => {
// create an audio instance and set the incoming stream
const audio = new Audio()
audio.srcObject = stream
audio.autoplay = true
// add the audio to peerAudios object if you want to address it for something
// later (volume, etc.)
peerAudios[peerId] = audio
}Doing the same with video is similar, just be sure to add incoming streams to video elements in the DOM:
const peerVideos = {}
const videoContainer = document.getElementById('videos')
room.onPeerStream = (stream, peerId) => {
let video = peerVideos[peerId]
// if this peer hasn't sent a stream before, create a video element
if (!video) {
video = document.createElement('video')
video.autoplay = true
// add video element to the DOM
videoContainer.appendChild(video)
}
video.srcObject = stream
peerVideos[peerId] = video
}Let's say your app supports sending various types of files and you want to annotate the raw bytes being sent with metadata about how they should be interpreted. Instead of manually adding metadata bytes to the buffer you can simply pass a metadata argument in the sender action for your binary payload:
const file = room.makeAction('file')
file.onMessage = (data, {peerId, metadata}) =>
console.log(
`got a file (${metadata.name}) from ${peerId} with type ${metadata.type}`,
data
)
file.send(buffer, {
metadata: {name: 'The CourierΚΌs Tragedy', type: 'application/pdf'}
})Action sender functions return a promise that resolves when they're done sending. You can optionally use this to indicate to the user when a large transfer is done.
await file.send(amplePayload)
console.log('done sending to all peers')Action sender functions also take an optional callback function that will be continuously called as the transmission progresses. This can be used for showing a progress bar to the sender for large transfers. The callback is called with a percentage value between 0 and 1 and the receiving peer's ID:
file.send(payload, {
target: [peerIdA, peerIdB, peerIdC],
metadata: {filename: 'paranoids.flac'},
onProgress: (percent, {peerId}) => (loadingBars[peerId].value = percent)
})Similarly you can listen for progress events as a receiver like this:
const file = room.makeAction('file')
file.onReceiveProgress = (percent, {peerId, metadata}) =>
console.log(
`${percent * 100}% done receiving ${metadata.filename} from ${peerId}`
)Notice that any metadata is sent with progress events so you can show the receiving user that there is a transfer in progress with perhaps the name of the incoming file.
Since a peer can send multiple transmissions in parallel, you can also use metadata to differentiate between them, e.g. by sending a unique ID.
Once peers are connected to each other all of their communications are
end-to-end encrypted. During the initial connection / discovery process, peers'
SDPs are sent via
the chosen peering strategy medium. By default the SDP is encrypted using a key
derived from your app ID and room ID to prevent plaintext session data from
appearing in logs. This is fine for most use cases, however a relay strategy
operator can reverse engineer the key using the room and app IDs. A more secure
option is to pass a password parameter in the app configuration object which
will be used to derive the encryption key:
joinRoom({appId: 'kinneret', password: 'MuchoMaa$'}, 'w_a_s_t_e__v_i_p')This is a shared secret that must be known ahead of time and the password must match for all peers in the room for them to be able to connect. An example use case might be a private chat room where users learn the password via external means.
Trystero functions are idempotent so they already work out of the box as React hooks.
Here's a simple example component where each peer syncs their favorite color to everyone else:
import {joinRoom} from 'trystero'
import {useState} from 'react'
const trysteroConfig = {appId: 'thurn-und-taxis'}
export default function App({roomId}) {
const room = joinRoom(trysteroConfig, roomId)
const colorAction = room.makeAction('color')
const [myColor, setMyColor] = useState('#c0ffee')
const [peerColors, setPeerColors] = useState({})
// whenever new peers join the room, send my color to them:
room.onPeerJoin = peer => colorAction.send(myColor, {target: peer})
// listen for peers sending their colors and update the state accordingly:
colorAction.onMessage = (color, {peerId}) =>
setPeerColors(peerColors => ({...peerColors, [peerId]: color}))
const updateColor = e => {
const {value} = e.target
// when updating my own color, broadcast it to all peers:
colorAction.send(value)
setMyColor(value)
}
return (
<>
<h1>Trystero + React</h1>
<h2>My color:</h2>
<input type="color" value={myColor} onChange={updateColor} />
<h2>Peer colors:</h2>
<ul>
{Object.entries(peerColors).map(([peerId, color]) => (
<li key={peerId} style={{backgroundColor: color}}>
{peerId}: {color}
</li>
))}
</ul>
</>
)
}Astute readers may notice the above example is simple and doesn't consider if we
want to change the component's room ID or unmount it. For those scenarios you
can use this simple useRoom() hook that unsubscribes from room events
accordingly:
import {joinRoom} from 'trystero'
import {useEffect, useRef} from 'react'
export const useRoom = (roomConfig, roomId) => {
const roomRef = useRef(joinRoom(roomConfig, roomId))
const lastRoomIdRef = useRef(roomId)
useEffect(() => {
if (roomId !== lastRoomIdRef.current) {
roomRef.current.leave()
roomRef.current = joinRoom(roomConfig, roomId)
lastRoomIdRef.current = roomId
}
return () => roomRef.current.leave()
}, [roomConfig, roomId])
return roomRef.current
}WebRTC is powerful but some networks simply don't allow direct P2P connections using it. If you find that certain user pairings aren't working in Trystero, you're likely encountering an issue at the network provider level. To solve this you can configure a TURN server which will act as a proxy layer for peers that aren't able to connect directly to one another.
- If you can, confirm that the issue is specific to particular network conditions (e.g. user with ISP X cannot connect to a user with ISP Y). If other user pairings are working (like those between two browsers on the same machine), this likely confirms that Trystero is working correctly.
- Sign up for a TURN service or host your own. There are various hosted TURN services you can find online like Cloudflare (which offers a free tier with 1,000 GB traffic per month) or Open Relay. You can also host an open source TURN server like coturn, Pion TURN, Violet, or eturnal. Keep in mind data will only go through the TURN server for peers that can't directly connect and will still be end-to-end encrypted.
- Once you have a TURN server, configure Trystero with it like this:
const room = joinRoom( { // ...your app config turnConfig: [ { // single string or list of strings of URLs to access TURN server urls: ['turn:your-turn-server.ok:1979'], username: 'username', credential: 'password' } ] }, 'roomId' )
Trystero works outside browsers too, like in Node or Bun. Why would you want to run something that helps you avoid servers on a server? One reason is if you want an always-on peer which can be useful for remembering the last state of data, broadcasting it to new users. Another reason might be to run peers that are lighter weight and don't need a full browser running, like an embedded device or Raspberry Pi.
Running server-side uses the same syntax as in the browser, but you need to import a polyfill for WebRTC support:
import {joinRoom} from 'trystero'
import {RTCPeerConnection} from 'werift'
const room = joinRoom(
{appId: 'your-app-id', rtcPolyfill: RTCPeerConnection},
'your-room-name'
)If you want a tiny relay that you control, use the WebSocket relay package. Start the relay on Node or Bun:
import {createWsRelayServer} from '@trystero-p2p/ws-relay/server'
createWsRelayServer({port: 8080})Then in the browser:
import {joinRoom} from '@trystero-p2p/ws-relay'
const room = joinRoom(
{
appId: 'app-id',
relayConfig: {
urls: ['wss://localhost:8080']
}
},
'room-id'
)The relayConfig.urls config option is required for this strategy because there
are no public default servers. You can pass multiple.
If you want to provide your own signaling backend, you can build a custom
strategy with createTopicStrategy. This is the recommended helper for simple
pub/sub relays because Trystero handles room lifecycle details like passive
mode, announce scheduling, and peer-specific signaling topics.
The example below assumes a WebSocket relay that does simple pub/sub routing:
- Client sends
{"type": "subscribe" | "unsubscribe" | "publish", "topic", "payload"} - Server broadcasts
{"topic", "payload"}to subscribers of each topic
This is just to show you how it works; if you want a simple self-hosted
solution, use the ws-relay package explained above.
import {createTopicStrategy, toJson} from '@trystero-p2p/core'
export const joinRoom = createTopicStrategy({
// Define init as a function that returns a promise of your signaling client.
// Resolve the promise when your client is ready to send messages.
// You can also return an array of client promises for redundancy.
// In this case, the client is a single WebSocket.
init: config =>
new Promise((resolve, reject) => {
const ws = new WebSocket(config.relayConfig.urls[0])
ws.addEventListener('open', () => resolve(ws), {once: true})
ws.addEventListener('error', reject, {once: true})
}),
// Subscribe to one topic. Trystero decides which topics are needed and when.
subscribeTopic: (client, topic, onMessage) => {
const onWsMessage = event => {
const message = JSON.parse(String(event.data))
if (message.topic !== topic) {
return
}
onMessage(message.topic, message.payload)
}
client.addEventListener('message', onWsMessage)
client.send(toJson({type: 'subscribe', topic}))
return () => {
client.send(toJson({type: 'unsubscribe', topic}))
client.removeEventListener('message', onWsMessage)
}
},
// Publish a payload to one topic.
publishTopic: (client, topic, payload) =>
client.send(
toJson({
type: 'publish',
topic,
payload
})
)
})
const room = joinRoom(
{appId: 'my-app-id', relayConfig: {urls: ['wss://my-relay.example']}},
'my-room-id'
)For non-pub/sub signaling protocols, such as trackers that exchange offers in
bulk, createStrategy is available as a lower-level advanced API.
To use the Supabase strategy:
- Create a Supabase project or use an existing one
- On the dashboard, go to Project Settings -> API
- Copy the Project URL and set that as the
appIdin the Trystero config, copy theanon publicAPI key and set it asrelayConfig.supabaseKeyin the Trystero config
If you want to use the Firebase strategy and don't have an existing project:
- Create a Firebase project
- Create a new Realtime Database
- Copy the
databaseURLand use it as theappIdin your Trystero config
Optional: configure the database with security rules to limit activity:
{
"rules": {
".read": false,
".write": false,
"__trystero__": {
".read": false,
".write": false,
"$room_id": {
".read": true,
".write": true
}
}
}
}These rules ensure room peer presence is only readable if the room namespace is known ahead of time.
Adds local user to room whereby other peers in the same namespace will open
communication channels and send events. Calling joinRoom() multiple times with
the same namespace will return the same room instance.
-
config- Configuration object containing the following keys:-
appId- (required) A unique string identifying your app. When using Supabase, this should be set to your project URL (see Supabase setup instructions). If using Firebase, this should be thedatabaseURLfrom your Firebase config (also seerelayConfig.firebaseAppbelow for an alternative way of configuring the Firebase strategy). -
password- (optional) A string to encrypt session descriptions via AES-GCM as they are passed through the peering medium. If not set, session descriptions will be encrypted with a key derived from the app ID and room name. A custom password must match between any peers in the room for them to connect. See encryption for more details. -
passive- (optional) Boolean for backup or relay peers that should listen for active peers without announcing themselves while a room is dormant. Passive peers activate only after hearing a non-passive peer and include a passive flag in their signaling so passive peers do not connect to each other. For BitTorrent, dormant passive rooms announce as seeders (left: 0) without offers, which avoids passive-to-passive discovery while keeping tracker load low. -
relayConfig- (optional unless required by your chosen strategy) Object containing strategy-specific relay settings:-
urls- (optional for π BitTorrent, π¦ Nostr, π‘ MQTT; required for π WebSocket relay) Custom list of URLs for the strategy to use to bootstrap P2P connections. These would be BitTorrent trackers, Nostr relays, MQTT brokers, and WebSocket relays, respectively. They must support secure WebSocket connections. -
redundancy- (optional, π BitTorrent, π¦ Nostr, π‘ MQTT only) Integer specifying how many default relay endpoints to connect to simultaneously. Passing aurlsoption will cause this option to be ignored as the entire list will be used. -
manualReconnection- (optional, π¦ Nostr and π BitTorrent only) Boolean (default:false) that when set totruedisables automatically pausing and resuming reconnection attempts when the browser goes offline and comes back online. This is useful if you want to manage this behavior yourself. -
supabaseKey- (required, β‘οΈ Supabase only) Your Supabase project'sanon publicAPI key. -
firebaseApp- (optional, π₯ Firebase only) You can pass an already initialized Firebase app instance instead of anappId. Normally Trystero will initialize a Firebase app based on theappIdbut this will fail if you've already initialized it for use elsewhere. -
firebasePath- (optional, π₯ Firebase only) String specifying path where Trystero writes its matchmaking data in your database ('__trystero__'by default). Changing this is useful if you want to run multiple apps using the same database and don't want to worry about namespace collisions.
-
-
rtcConfig- (optional) Specifies a customRTCConfigurationfor all peer connections. -
trickleIce- (optional) Boolean controlling whether ICE candidates are sent incrementally (true) or bundled with SDP (false). Default is strategy-dependent:truefor most strategies,falsefor BitTorrent and IPFS unless explicitly set. -
turnConfig- (optional) Specifies a custom list of TURN servers to use (see Troubleshooting connection issues). Each item in the list should correspond to an ICE server config object. When passing a TURN config like this, Trystero's default STUN servers will also be used. To override this and use both custom STUN and TURN servers, instead pass the config via the abovertcConfig.iceServersoption as a list of both STUN/TURN servers β this won't inherit Trystero's defaults. -
rtcPolyfill- (optional) Use this to pass a customRTCPeerConnection-compatible constructor. This is useful for running outside of a browser, such as in Node (still experimental).
-
-
roomId- A string to namespace peers and events within a room. -
callbacks- (optional) Callback config object containing:-
onJoinError(details)- Called when room join fails due to an incorrect password, when handshake admission fails (including timeout), or when peers exchange SDP but WebRTC still cannot establish a direct connection. This last case usually means TURN servers are needed or misconfigured (see Troubleshooting connection issues).detailsis an object containingappId,roomId,peerId, anderrordescribing the failure. -
onPeerHandshake(peerId, send, receive, isInitiator)- Async predicate that runs after the transport connects but before the peer becomes active. Return/resolve to accept the peer, throw/reject to deny the peer.peerId- ID of the pending peer.send(data, [metadata])- Sends handshake payloads to the pending peer.receive()- Resolves to the next handshake message from the peer as{data, metadata}.isInitiator- Deterministic role flag for avoiding protocol deadlocks.
-
handshakeTimeoutMs- Timeout for pending handshakes in milliseconds (10000by default). If exceeded, the peer is denied andonJoinErroris called.
During handshake, the peer remains pending and is not included in
getPeers()and does not trigger Trystero API events (onPeerJoin, action receivers, stream/track callbacks). Non-handshake data received while pending is dropped.Minimal handshake example:
import {joinRoom} from 'trystero' const room = joinRoom({appId: 'my-app'}, 'secure-room', { onPeerHandshake: async (_, send, receive, isInitiator) => { if (isInitiator) { await send({challenge: 'prove-you-know-the-secret'}) const {data} = await receive() if (data?.response !== 'shared-secret') { throw new Error('handshake rejected') } } else { const {data} = await receive() if (data?.challenge !== 'prove-you-know-the-secret') { throw new Error('handshake rejected') } await send({response: 'shared-secret'}) } } })
-
Returns an object with the following methods:
-
Remove local user from room and unsubscribe from room events.
-
Returns a map of
RTCPeerConnections for the peers present in room (not including the local user). The keys of this object are the respective peers' IDs. -
Returns whether the room was joined with
passive: true. -
Broadcasts media stream to other peers.
-
stream- AMediaStreamwith audio and/or video to send to peers in the room. -
options.target- (optional) If specified, the stream is sent only to the target peer ID (string) or list of peer IDs (array). Passingnullor omitting this option sends to all peers in the room. -
options.metadata- (optional) Additional metadata (any serializable type) to be sent with the stream. This is useful when sending multiple streams so recipients know which is which (e.g. a webcam versus a screen capture).
-
-
Stops sending previously sent media stream to other peers.
-
stream- A previously sentMediaStreamto stop sending. -
options.target- (optional) If specified, the stream is removed only from the target peer ID (string) or list of peer IDs (array).
-
-
Adds a new media track to a stream.
-
track- AMediaStreamTrackto add to an existing stream. -
stream- The targetMediaStreamto attach the new track to. -
options.target- (optional) If specified, the track is sent only to the target peer ID (string) or list of peer IDs (array). -
options.metadata- (optional) Additional metadata (any serializable type) to be sent with the track. Seemetadatanotes foraddStream()above for more details.
-
-
Removes a media track.
-
track- TheMediaStreamTrackto remove. -
options.target- (optional) If specified, the track is removed only from the target peer ID (string) or list of peer IDs (array).
-
-
Replaces a media track with a new one.
-
oldTrack- TheMediaStreamTrackto remove. -
newTrack- AMediaStreamTrackto attach. -
options.target- (optional) If specified, the track is replaced only for the target peer ID (string) or list of peer IDs (array). -
options.metadata- (optional) Additional metadata (any serializable type) to be sent with the replacement track.
-
-
A callback property that will be called when a peer joins the room. Assigning a new function replaces the previous handler; assigning
nullclears it. Existing active peers are immediately replayed to a newly assigned handler.callback(peerId)- Function to run whenever a peer joins, called with the peer's ID.
Example:
room.onPeerJoin = peerId => console.log(`${peerId} joined`)
-
A callback property that will be called when a peer leaves the room. Assigning a new function replaces the previous handler; assigning
nullclears it.callback(peerId)- Function to run whenever a peer leaves, called with the peer's ID.
Example:
room.onPeerLeave = peerId => console.log(`${peerId} left`)
-
A callback property that will be called when a peer sends a media stream. Assigning a new function replaces the previous handler; assigning
nullclears it.callback(stream, peerId, metadata)- Function to run whenever a peer sends a media stream, called with the the peer's stream, ID, and optional metadata (seeaddStream()above for details).
Example:
room.onPeerStream = (stream, peerId) => console.log(`got stream from ${peerId}`, stream)
-
A callback property that will be called when a peer sends a media track. Assigning a new function replaces the previous handler; assigning
nullclears it.callback(track, stream, peerId, metadata)- Function to run whenever a peer sends a media track, called with the the peer's track, attached stream, ID, and optional metadata (seeaddTrack()above for details).
Example:
room.onPeerTrack = (track, stream, peerId) => console.log(`got track from ${peerId}`, track)
-
Listen for and send custom data actions.
actionId- A string to register this action consistently among all peers.
If
config.kindis omitted, the action is a one-way message action. Passingkind: 'request'creates a request/response action.Message actions expose:
send(data, [options])- Sends data to peers and resolves when local sending is complete.onMessage- Nullable callback property for received messages.onReceiveProgress- Nullable callback property for inbound progress.
Request actions expose:
request(data, options)- Sends to one peer and resolves with its response.requestMany(data, options)- Sends to many peers and resolves with peer-labeled settled results.onRequest- Nullable callback property that returns the response.onReceiveProgress- Nullable callback property for inbound progress.
Send options use
target,metadata,onProgress, andsignal. Request options usetarget,metadata,timeoutMs,onProgress, andsignal.requestMany()usestargetsplus optionalonResultfor each peer result as it arrives.Example:
const cursor = room.makeAction('cursormove') window.addEventListener('mousemove', e => cursor.send([e.clientX, e.clientY])) cursor.onMessage = ([x, y], {peerId}) => { const peerCursor = cursorMap[peerId] peerCursor.style.left = x + 'px' peerCursor.style.top = y + 'px' }
-
Takes a peer ID and returns a promise that resolves to the milliseconds the round-trip to that peer took. Use this for measuring latency.
peerId- Peer ID string of the target peer.
Example:
// log round-trip time every 2 seconds room.onPeerJoin = peerId => setInterval( async () => console.log(`took ${await room.ping(peerId)}ms`), 2000 )
A unique ID string other peers will know the local user as globally across rooms.
(π BitTorrent, π¦ Nostr, π‘ MQTT, π WebSocket relay only) Returns an object of relay URL keys mapped to their WebSocket connections. This can be useful for determining the state of the user's connection to the relays and handling any connection failures.
Example:
import {getRelaySockets} from '@trystero-p2p/torrent'
console.log(getRelaySockets())
// => Object {
// "wss://tracker.webtorrent.dev": WebSocket,
// "wss://tracker.openwebtorrent.com": WebSocket
// }(π¦ Nostr, π BitTorrent only) Normally Trystero will try to automatically
reconnect to relay sockets unless relayConfig.manualReconnection: true is set
in the room config. Calling this function stops relay reconnection attempts
until resumeRelayReconnection() is called.
(π¦ Nostr, π BitTorrent only) Allows relay reconnection attempts to resume.
(See pauseRelayReconnection() above).
By default Trystero uses the Nostr network which is highly decentralized with hundreds of active relays running. This is a good choice if you're interested in decentralization and high redundancy. The other decentralized strategies are recommended in the order of MQTT, BitTorrent, and IPFS, based on robustness. These networks have far less relay redundancy than Nostr, but you might prefer them for other reasons. You can of course host your own relay server for any of these strategies.
For a middleground between using public relays and self-hosting, the built-in Supabase and Firebase strategies are a good option.
Trystero by Dan Motzenbecker