{"id":13691666,"url":"https://github.com/dmotz/trystero","last_synced_at":"2026-10-19T07:25:13.243Z","repository":{"id":44451460,"uuid":"307219355","full_name":"dmotz/trystero","owner":"dmotz","description":"✨🤝✨ Build instant multiplayer webapps, no server required — Magic WebRTC matchmaking over BitTorrent, Nostr, MQTT, IPFS, Supabase, and Firebase","archived":false,"fork":false,"pushed_at":"2026-10-05T01:06:10.000Z","size":1972,"stargazers_count":2784,"open_issues_count":7,"forks_count":160,"subscribers_count":31,"default_branch":"main","last_synced_at":"2026-10-13T19:43:47.443Z","etag":null,"topics":["bittorrent","chat","decentralized","firebase","ipfs","javascript","matchmaking","mqtt","nostr","p2p","peer-to-peer","realtime","serverless","signaling","signalling","supabase","web3","webrtc","webtorrent"],"latest_commit_sha":null,"homepage":"https://trystero.dev","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dmotz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"claude":null,"gemini":null,"cursor":null,"copilot":null,"dco":null,"cla":null,"disclosure":null}},"created_at":"2020-10-26T00:11:07.000Z","updated_at":"2026-10-13T18:06:41.000Z","dependencies_parsed_at":"2026-10-04T17:28:18.674Z","dependency_job_id":null,"html_url":"https://github.com/dmotz/trystero","commit_stats":{"total_commits":831,"total_committers":5,"mean_commits":166.2,"dds":"0.028880866425992746","last_synced_commit":"8eaaafb71cbb5272a6c597f0212cbfb4bed0110c"},"previous_names":[],"tags_count":72,"template":false,"template_full_name":null,"purl":"pkg:github/dmotz/trystero","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmotz%2Ftrystero","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmotz%2Ftrystero/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmotz%2Ftrystero/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmotz%2Ftrystero/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmotz","download_url":"https://codeload.github.com/dmotz/trystero/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmotz%2Ftrystero/sbom","scorecard":{"id":348283,"data":{"date":"2025-08-11","repo":{"name":"github.com/dmotz/trystero","commit":"77abcaa4955aa3b64eb47b8fe6268d5b932203bf"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.7,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Code-Review","score":0,"reason":"Found 1/28 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":10,"reason":"30 commit(s) and 8 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":0,"reason":"Project has not signed or included provenance with any releases.","details":["Warn: release artifact 0.21.4 not signed: https://api.github.com/repos/dmotz/trystero/releases/220870765","Warn: release artifact 0.21.3 not signed: https://api.github.com/repos/dmotz/trystero/releases/213417097","Warn: release artifact 0.21.2 not signed: https://api.github.com/repos/dmotz/trystero/releases/212973195","Warn: release artifact 0.21.1 not signed: https://api.github.com/repos/dmotz/trystero/releases/210248266","Warn: release artifact 0.21.0 not signed: https://api.github.com/repos/dmotz/trystero/releases/208542654","Warn: release artifact 0.21.4 does not have provenance: https://api.github.com/repos/dmotz/trystero/releases/220870765","Warn: release artifact 0.21.3 does not have provenance: https://api.github.com/repos/dmotz/trystero/releases/213417097","Warn: release artifact 0.21.2 does not have provenance: https://api.github.com/repos/dmotz/trystero/releases/212973195","Warn: release artifact 0.21.1 does not have provenance: https://api.github.com/repos/dmotz/trystero/releases/210248266","Warn: release artifact 0.21.0 does not have provenance: https://api.github.com/repos/dmotz/trystero/releases/208542654"],"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 3 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"13 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-xffm-g5w8-qvg7","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-2p57-rm9w-gvfp","Warn: Project is vulnerable to: GHSA-76c9-3jph-rj3q","Warn: Project is vulnerable to: GHSA-3j8f-xvm3-ffx4","Warn: Project is vulnerable to: GHSA-4p35-cfcx-8653","Warn: Project is vulnerable to: GHSA-7f3x-x4pr-wqhj","Warn: Project is vulnerable to: GHSA-jpp7-7chh-cf67","Warn: Project is vulnerable to: GHSA-q6wq-5p59-983w","Warn: Project is vulnerable to: GHSA-j9fq-vwqv-2fm2","Warn: Project is vulnerable to: GHSA-pqw5-jmp5-px4v","Warn: Project is vulnerable to: GHSA-7p7h-4mm5-852v","Warn: Project is vulnerable to: GHSA-p9pc-299p-vxgp"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-18T07:42:04.768Z","repository_id":44451460,"created_at":"2025-08-18T07:42:04.768Z","updated_at":"2025-08-18T07:42:04.768Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":344872684,"owners_count":38351258,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-10-03T21:59:58.778Z","status":"online","status_checked_at":"2026-10-19T02:00:06.414Z","response_time":53,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bittorrent","chat","decentralized","firebase","ipfs","javascript","matchmaking","mqtt","nostr","p2p","peer-to-peer","realtime","serverless","signaling","signalling","supabase","web3","webrtc","webtorrent"],"created_at":"2024-08-02T17:00:48.606Z","updated_at":"2026-10-19T07:25:13.228Z","avatar_url":"https://github.com/dmotz.png","language":"TypeScript","funding_links":[],"categories":["JavaScript","webrtc","Official links","TypeScript","Libraries"],"sub_categories":["JavaScript"],"readme":"# ✨🤝✨ Trystero\n\n**Build instant multiplayer web apps, no server required**\n\n👉 **[Try it live on trystero.dev](https://trystero.dev)** 👈\n\nTrystero makes browsers discover each other and communicate directly. No\naccounts. No deploying infrastructure. Just import and connect.\n\nPeers can connect via 🌊 BitTorrent, 🐦 Nostr, 📡 MQTT, ⚡️ Supabase, 🔥Firebase,\n🪐 IPFS, or a 🔌 self-hosted WebSocket relay – all using the same API.\n\nBesides making peer matching automatic, Trystero offers some nice abstractions\non top of WebRTC:\n\n- 👂📣 Rooms / broadcasting\n- 🔢📩 Automatic serialization / deserialization of data\n- 🎥🏷 Attach metadata to binary data and media streams\n- ✂️⏳ Automatic chunking and throttling of large data\n- ⏱🤞 Progress events and promises for data transfers\n- 🔐📝 Session data encryption\n- 🏭⚡ Can run peers server-side on Node, Bun, and Deno\n- ⚛️🪝 React hooks\n\nYou can see what people are building with Trystero\n[here](https://github.com/jeremyckahn/awesome-trystero).\n\n---\n\n## Contents\n\n- [How it works](#how-it-works)\n- [Get started](#get-started)\n- [Listen for events](#listen-for-events)\n- [Broadcast events](#broadcast-events)\n- [Audio and video](#audio-and-video)\n- [Advanced](#advanced)\n  - [Binary metadata](#binary-metadata)\n  - [Receiving files](#receiving-files)\n  - [Action promises](#action-promises)\n  - [Progress updates](#progress-updates)\n  - [Encryption](#encryption)\n  - [React hooks](#react-hooks)\n  - [Troubleshooting connection issues](#troubleshooting-connection-issues)\n  - [Running server-side (Node, Bun, Deno)](#running-server-side-node-bun-deno)\n  - [Self-hosted WebSocket relay](#self-hosted-websocket-relay)\n  - [Write your own strategy](#write-your-own-strategy)\n  - [Supabase setup](#supabase-setup)\n  - [Firebase setup](#firebase-setup)\n- [API](#api)\n  - [`joinRoom(config, roomId, [callbacks])`](#joinroomconfig-roomid-callbacks)\n  - [`selfId`](#selfid)\n  - [`getRelaySockets()`](#getrelaysockets)\n  - [`pauseRelayReconnection()`](#pauserelayreconnection)\n  - [`resumeRelayReconnection()`](#resumerelayreconnection)\n- [Which strategy should I choose?](#which-strategy-should-i-choose)\n\n---\n\n## How it works\n\n👉 **If you just want to try out Trystero, you can skip this explainer and\n[jump into using it](#get-started).**\n\nTo establish a direct peer-to-peer connection with WebRTC, a signalling channel\nis needed to exchange peer information\n([SDP](https://en.wikipedia.org/wiki/Session_Description_Protocol)). Typically\nthis involves running your own matchmaking server but Trystero abstracts this\naway for you and offers multiple strategies for connecting peers (currently\nBitTorrent, Nostr, MQTT, Supabase, Firebase, IPFS, and self-hosted WebSocket\nrelay).\n\nThe important point to remember is this:\n\n\u003e 🔒\n\u003e\n\u003e Beyond peer discovery, your app's data never touches the strategy medium and\n\u003e is sent directly peer-to-peer and end-to-end encrypted between users.\n\u003e\n\u003e 👆\n\nYou can [compare strategies here](#which-strategy-should-i-choose).\n\n## Get started\n\nInstall Trystero with your preferred package manager, then import it in your\ncode:\n\n```sh\nnpm i trystero\n```\n\n```js\nimport {joinRoom} from 'trystero'\n```\n\nNo package manager? You can also use a CDN:\n\n```html\n\u003cscript type=\"module\"\u003e\n  import {joinRoom} from 'https://esm.run/trystero'\n\u003c/script\u003e\n```\n\nThe default Trystero package runs on the Nostr network, but you can swap in any\nother stategy by changing which package you import:\n\n```js\nimport {joinRoom} from '@trystero-p2p/mqtt'\n// or\nimport {joinRoom} from '@trystero-p2p/torrent'\n// or\nimport {joinRoom} from '@trystero-p2p/supabase'\n// or\nimport {joinRoom} from '@trystero-p2p/firebase'\n// or\nimport {joinRoom} from '@trystero-p2p/ipfs'\n// or\nimport {joinRoom} from '@trystero-p2p/ws-relay'\n```\n\nNext, join the user to a room with an ID:\n\n```js\nconst config = {appId: 'san_narciso_3d'}\nconst room = joinRoom(config, 'yoyodyne')\n```\n\nThe first argument is a configuration object that requires an `appId`. This\nshould be a completely unique identifier for your app¹. The second argument is\nthe room ID.\n\n\u003e Why rooms? Browsers can only handle a limited amount of WebRTC connections at\n\u003e a time so it's recommended to design your app such that users are divided into\n\u003e groups (or rooms, or namespaces, or channels... whatever you'd like to call\n\u003e them).\n\n¹ When using Firebase, `appId` should be your `databaseURL` and when using\nSupabase, it should be your project URL.\n\n## Listen for events\n\nListen for peers joining the room:\n\n```js\nroom.onPeerJoin = peerId =\u003e console.log(`${peerId} joined`)\n```\n\nListen for peers leaving the room:\n\n```js\nroom.onPeerLeave = peerId =\u003e console.log(`${peerId} left`)\n```\n\nListen for peers sending their audio/video streams:\n\n```js\nroom.onPeerStream = (stream, peerId) =\u003e\n  (peerElements[peerId].video.srcObject = stream)\n```\n\nTo unsubscribe from events, leave the room:\n\n```js\nroom.leave()\n```\n\nYou can access the local user's peer ID by importing `selfId` like so:\n\n```js\nimport {selfId} from 'trystero'\n\nconsole.log(`my peer ID is ${selfId}`)\n```\n\n## Broadcast events\n\nSend peers your video stream:\n\n```js\nconst stream = await navigator.mediaDevices.getUserMedia({\n  audio: true,\n  video: true\n})\nroom.addStream(stream)\n```\n\nSend and subscribe to custom peer-to-peer actions:\n\n```js\nconst drink = room.makeAction('drink')\n\n// buy drink for a friend\ndrink.send({drink: 'negroni', withIce: true}, {target: friendId})\n\n// buy round for the house\ndrink.send({drink: 'mezcal', withIce: false})\n\n// listen for drinks sent to you\ndrink.onMessage = (data, {peerId}) =\u003e\n  console.log(\n    `got a ${data.drink} with${data.withIce ? '' : 'out'} ice from ${peerId}`\n  )\n```\n\nActions can also use request/response semantics:\n\n```js\nconst isEven = room.makeAction('is-even', {\n  kind: 'request',\n  onRequest: n =\u003e n % 2 === 0\n})\n\nconst result = await isEven.request(42, {\n  target: friendId,\n  timeoutMs: 1000\n})\n```\n\nTo ask multiple peers at once, use `requestMany()`. It resolves with a\npeer-labeled result for every target, while `onResult` lets you react as each\npeer answers:\n\n```js\nconst availability = room.makeAction('availability', {\n  kind: 'request',\n  onRequest: ({date}) =\u003e calendar.isFree(date)\n})\n\nconst results = await availability.requestMany(\n  {date: '2026-05-04'},\n  {\n    targets: teammateIds,\n    timeoutMs: 1000,\n    onResult: result =\u003e {\n      if (result.status === 'fulfilled') {\n        updateAvailabilityBadge(result.peerId, result.value)\n      }\n    }\n  }\n)\n\nconst freePeers = results\n  .filter(result =\u003e result.status === 'fulfilled' \u0026\u0026 result.value)\n  .map(result =\u003e result.peerId)\n```\n\nIf you're using TypeScript, you can add a type hint to the action:\n\n```typescript\ntype CursorMove = {x: number; y: number}\n\nconst cursor = room.makeAction\u003cCursorMove\u003e('cursor-move')\n```\n\nYou can also use actions to send binary data, like images:\n\n```js\nconst pic = room.makeAction('pic')\n\n// blobs are automatically handled, as are any form of TypedArray\ncanvas.toBlob(blob =\u003e pic.send(blob))\n\n// binary data is received as raw ArrayBuffers so your handling code should\n// interpret it in a way that makes sense\npic.onMessage = (data, {peerId}) =\u003e\n  (imgs[peerId].src = URL.createObjectURL(new Blob([data])))\n```\n\nLet's say we want users to be able to name themselves:\n\n```js\nconst idsToNames = {}\nconst name = room.makeAction('name')\n\n// tell new peers your name when they connect\nroom.onPeerJoin = peerId =\u003e name.send('Oedipa', {target: peerId})\n\n// listen for peers naming themselves\nname.onMessage = (value, {peerId}) =\u003e (idsToNames[peerId] = value)\n\n// tell all peers at once when your name changes\nnameInput.addEventListener('change', e =\u003e name.send(e.target.value))\n\nroom.onPeerLeave = peerId =\u003e\n  console.log(`${idsToNames[peerId] || 'a weird stranger'} left`)\n```\n\n\u003e Actions are smart and handle serialization and chunking for you behind the\n\u003e scenes. This means you can send very large files and whatever data you send\n\u003e will be received on the other side as the same type (a number as a number, a\n\u003e string as a string, an object as an object, binary as binary, etc.). See\n\u003e [receiving files](#receiving-files) for optional size limits and acceptance\n\u003e callbacks.\n\n## Audio and video\n\nHere's a simple example of how you could create an audio chatroom:\n\n```js\n// this object can store audio instances for later\nconst peerAudios = {}\n\n// get a local audio stream from the microphone\nconst selfStream = await navigator.mediaDevices.getUserMedia({\n  audio: true,\n  video: false\n})\n\n// send stream to peers currently in the room\nroom.addStream(selfStream)\n\n// send stream to peers who join later\nroom.onPeerJoin = peerId =\u003e room.addStream(selfStream, {target: peerId})\n\n// handle streams from other peers\nroom.onPeerStream = (stream, peerId) =\u003e {\n  // create an audio instance and set the incoming stream\n  const audio = new Audio()\n  audio.srcObject = stream\n  audio.autoplay = true\n\n  // add the audio to peerAudios object if you want to address it for something\n  // later (volume, etc.)\n  peerAudios[peerId] = audio\n}\n```\n\nDoing the same with video is similar, just be sure to add incoming streams to\nvideo elements in the DOM:\n\n```js\nconst peerVideos = {}\nconst videoContainer = document.getElementById('videos')\n\nroom.onPeerStream = (stream, peerId) =\u003e {\n  let video = peerVideos[peerId]\n\n  // if this peer hasn't sent a stream before, create a video element\n  if (!video) {\n    video = document.createElement('video')\n    video.autoplay = true\n\n    // add video element to the DOM\n    videoContainer.appendChild(video)\n  }\n\n  video.srcObject = stream\n  peerVideos[peerId] = video\n}\n```\n\n## Advanced\n\n### Binary metadata\n\nLet's say your app supports sending various types of files and you want to\nannotate the raw bytes being sent with metadata about how they should be\ninterpreted. Instead of manually adding metadata bytes to the buffer you can\nsimply pass a metadata argument in the sender action for your binary payload:\n\n```js\nconst file = room.makeAction('file')\n\nfile.onMessage = (data, {peerId, metadata}) =\u003e\n  console.log(\n    `got a file (${metadata.name}) from ${peerId} with type ${metadata.type}`,\n    data\n  )\n\nfile.send(buffer, {\n  metadata: {name: 'The Courierʼs Tragedy', type: 'application/pdf'}\n})\n```\n\n### Receiving files\n\nIncoming data is accepted automatically. Use `onReceive` to accept or decline\npayloads based on their size, sender, or metadata:\n\n```js\nconst file = room.makeAction('file', {\n  onReceive: ({byteLength}) =\u003e byteLength \u003c= 100 * 1024 ** 2,\n  onMessage: (data, {metadata}) =\u003e saveFile(data, metadata.name)\n})\n```\n\nReturn `true` or `false`, or a promise of either. Large payloads wait for\napproval before sending their contents; declined small messages are discarded\nafter arrival. You can also assign `file.onReceive` later or set it to `null` to\nrestore automatic acceptance.\n\n`byteLength` is the payload size in bytes (UTF-8 for strings/JSON), excluding\nmetadata. The context's `signal` aborts when the transfer ends; use it to\ndismiss pending approval UI.\n\nEach room defaults to a 256 MiB receive budget. Set `maxReceiveBytes` in the\nroom config to allow larger files or reduce memory use. Concurrent transfers\nqueue automatically when the budget is occupied. Received files must fit in\nmemory.\n\n### Action promises\n\nSender promises resolve when local sending finishes; they do not confirm\ndelivery or application handling. If a receiver declines a large transfer, the\npromise rejects with `error.kind === 'rejected'`. Use request actions when you\nneed a response.\n\n```js\nawait file.send(amplePayload)\nconsole.log('done sending to all peers')\n```\n\n### Progress updates\n\nAction sender functions also take an optional callback function that will be\ncontinuously called as the transmission progresses. This can be used for showing\na progress bar to the sender for large transfers. The callback is called with a\npercentage value between 0 and 1 and the receiving peer's ID:\n\n```js\nfile.send(payload, {\n  target: [peerIdA, peerIdB, peerIdC],\n  metadata: {filename: 'paranoids.flac'},\n  onProgress: (percent, {peerId}) =\u003e (loadingBars[peerId].value = percent)\n})\n```\n\nSimilarly you can listen for progress events as a receiver like this:\n\n```js\nconst file = room.makeAction('file')\n\nfile.onReceiveProgress = (percent, {peerId, metadata}) =\u003e\n  console.log(\n    `${percent * 100}% done receiving ${metadata.filename} from ${peerId}`\n  )\n```\n\nNotice that any metadata is sent with progress events so you can show the\nreceiving user that there is a transfer in progress with perhaps the name of the\nincoming file.\n\nSince a peer can send multiple transmissions in parallel, you can also use\nmetadata to differentiate between them, e.g. by sending a unique ID.\n\n### Encryption\n\nOnce peers are connected to each other all of their communications are\nend-to-end encrypted. During the initial connection / discovery process, peers'\n[SDPs](https://en.wikipedia.org/wiki/Session_Description_Protocol) are sent via\nthe chosen peering strategy medium. By default the SDP is encrypted using a key\nderived from your app ID and room ID to prevent plaintext session data from\nappearing in logs. This is fine for most use cases, however a relay strategy\noperator can reverse engineer the key using the room and app IDs. A more secure\noption is to pass a `password` parameter in the app configuration object which\nwill be used to derive the encryption key:\n\n```js\njoinRoom({appId: 'kinneret', password: 'MuchoMaa$'}, 'w_a_s_t_e__v_i_p')\n```\n\nThis is a shared secret that must be known ahead of time and the password must\nmatch for all peers in the room for them to be able to connect. An example use\ncase might be a private chat room where users learn the password via external\nmeans.\n\n### React hooks\n\nTrystero functions are idempotent so they already work out of the box as React\nhooks.\n\nHere's a simple example component where each peer syncs their favorite color to\neveryone else:\n\n```jsx\nimport {joinRoom} from 'trystero'\nimport {useState} from 'react'\n\nconst trysteroConfig = {appId: 'thurn-und-taxis'}\n\nexport default function App({roomId}) {\n  const room = joinRoom(trysteroConfig, roomId)\n  const colorAction = room.makeAction('color')\n  const [myColor, setMyColor] = useState('#c0ffee')\n  const [peerColors, setPeerColors] = useState({})\n\n  // whenever new peers join the room, send my color to them:\n  room.onPeerJoin = peer =\u003e colorAction.send(myColor, {target: peer})\n\n  // listen for peers sending their colors and update the state accordingly:\n  colorAction.onMessage = (color, {peerId}) =\u003e\n    setPeerColors(peerColors =\u003e ({...peerColors, [peerId]: color}))\n\n  const updateColor = e =\u003e {\n    const {value} = e.target\n\n    // when updating my own color, broadcast it to all peers:\n    colorAction.send(value)\n    setMyColor(value)\n  }\n\n  return (\n    \u003c\u003e\n      \u003ch1\u003eTrystero + React\u003c/h1\u003e\n\n      \u003ch2\u003eMy color:\u003c/h2\u003e\n      \u003cinput type=\"color\" value={myColor} onChange={updateColor} /\u003e\n\n      \u003ch2\u003ePeer colors:\u003c/h2\u003e\n      \u003cul\u003e\n        {Object.entries(peerColors).map(([peerId, color]) =\u003e (\n          \u003cli key={peerId} style={{backgroundColor: color}}\u003e\n            {peerId}: {color}\n          \u003c/li\u003e\n        ))}\n      \u003c/ul\u003e\n    \u003c/\u003e\n  )\n}\n```\n\nAstute readers may notice the above example is simple and doesn't consider if we\nwant to change the component's room ID or unmount it. For those scenarios you\ncan use this simple `useRoom()` hook that unsubscribes from room events\naccordingly:\n\n```js\nimport {joinRoom} from 'trystero'\nimport {useEffect, useRef} from 'react'\n\nexport const useRoom = (roomConfig, roomId) =\u003e {\n  const roomRef = useRef(joinRoom(roomConfig, roomId))\n  const lastRoomIdRef = useRef(roomId)\n\n  useEffect(() =\u003e {\n    if (roomId !== lastRoomIdRef.current) {\n      roomRef.current.leave()\n      roomRef.current = joinRoom(roomConfig, roomId)\n      lastRoomIdRef.current = roomId\n    }\n\n    return () =\u003e roomRef.current.leave()\n  }, [roomConfig, roomId])\n\n  return roomRef.current\n}\n```\n\n### Troubleshooting connection issues\n\nWebRTC is powerful but some networks simply don't allow direct P2P connections\nusing it. If you find that certain user pairings aren't working in Trystero,\nyou're likely encountering an issue at the network provider level. To solve this\nyou can configure a TURN server which will act as a proxy layer for peers that\naren't able to connect directly to one another.\n\n1. If you can, confirm that the issue is specific to particular network\n   conditions (e.g. user with ISP X cannot connect to a user with ISP Y). If\n   other user pairings are working (like those between two browsers on the same\n   machine), this likely confirms that Trystero is working correctly.\n2. Sign up for a TURN service or host your own. There are various hosted TURN\n   services you can find online like\n   [Cloudflare](https://developers.cloudflare.com/calls/turn/) (which offers a\n   free tier with 1,000 GB traffic per month) or\n   [Open Relay](https://www.metered.ca/stun-turn). You can also host an open\n   source TURN server like [coturn](https://github.com/coturn/coturn),\n   [Pion TURN](https://github.com/pion/turn),\n   [Violet](https://github.com/paullouisageneau/violet), or\n   [eturnal](https://github.com/processone/eturnal). Keep in mind data will only\n   go through the TURN server for peers that can't directly connect and will\n   still be end-to-end encrypted.\n3. Once you have a TURN server, configure Trystero with it like this:\n   ```js\n   const room = joinRoom(\n     {\n       // ...your app config\n       turnConfig: [\n         {\n           // single string or list of strings of URLs to access TURN server\n           urls: ['turn:your-turn-server.ok:1979'],\n           username: 'username',\n           credential: 'password'\n         }\n       ]\n     },\n     'roomId'\n   )\n   ```\n\n### Running server-side (Node, Bun, Deno)\n\nTrystero works outside browsers too, like in Node, Bun, or Deno. Why would you\nwant to run something that helps you avoid servers on a server? One reason is if\nyou want an always-on peer which can be useful for remembering the last state of\ndata, broadcasting it to new users. Another reason might be to run peers that\nare lighter weight and don't need a full browser running, like an embedded\ndevice or Raspberry Pi.\n\nRunning server-side uses the same syntax as in the browser, but you need to\nimport a polyfill for WebRTC support:\n\n```js\nimport {joinRoom} from 'trystero'\nimport {RTCPeerConnection} from 'werift'\n\nconst room = joinRoom(\n  {appId: 'your-app-id', rtcPolyfill: RTCPeerConnection},\n  'your-room-name'\n)\n```\n\n### Self-hosted WebSocket relay\n\nIf you want a tiny relay that you control, use the WebSocket relay package.\nStart the relay on Node, Bun, or Deno:\n\n```js\nimport {createWsRelayServer} from '@trystero-p2p/ws-relay/server'\n\ncreateWsRelayServer({port: 8080})\n```\n\nThen in the browser:\n\n```js\nimport {joinRoom} from '@trystero-p2p/ws-relay'\n\nconst room = joinRoom(\n  {\n    appId: 'app-id',\n    relayConfig: {\n      urls: ['wss://localhost:8080']\n    }\n  },\n  'room-id'\n)\n```\n\nThe `relayConfig.urls` config option is required for this strategy because there\nare no public default servers. You can pass multiple.\n\n### Write your own strategy\n\nIf you want to provide your own signaling backend, you can build a custom\nstrategy with `createTopicStrategy`. This is the recommended helper for simple\npub/sub relays because Trystero handles room lifecycle details like passive\nmode, announce scheduling, and peer-specific signaling topics.\n\nThe example below assumes a WebSocket relay that does simple pub/sub routing:\n\n- Client sends\n  `{\"type\": \"subscribe\" | \"unsubscribe\" | \"publish\", \"topic\", \"payload\"}`\n- Server broadcasts `{\"topic\", \"payload\"}` to subscribers of each topic\n\nThis is just to show you how it works; if you want a simple self-hosted\nsolution, use the `ws-relay` package explained above.\n\n```js\nimport {createTopicStrategy, toJson} from '@trystero-p2p/core'\n\nexport const joinRoom = createTopicStrategy({\n  // Define init as a function that returns a promise of your signaling client.\n  // Resolve the promise when your client is ready to send messages.\n  // You can also return an array of client promises for redundancy.\n  // In this case, the client is a single WebSocket.\n  init: config =\u003e\n    new Promise((resolve, reject) =\u003e {\n      const ws = new WebSocket(config.relayConfig.urls[0])\n      ws.addEventListener('open', () =\u003e resolve(ws), {once: true})\n      ws.addEventListener('error', reject, {once: true})\n    }),\n\n  // Subscribe to one topic. Trystero decides which topics are needed and when.\n  subscribeTopic: (client, topic, onMessage) =\u003e {\n    const onWsMessage = event =\u003e {\n      const message = JSON.parse(String(event.data))\n\n      if (message.topic !== topic) {\n        return\n      }\n\n      onMessage(message.topic, message.payload)\n    }\n\n    client.addEventListener('message', onWsMessage)\n    client.send(toJson({type: 'subscribe', topic}))\n\n    return () =\u003e {\n      client.send(toJson({type: 'unsubscribe', topic}))\n      client.removeEventListener('message', onWsMessage)\n    }\n  },\n\n  // Publish a payload to one topic.\n  publishTopic: (client, topic, payload) =\u003e\n    client.send(\n      toJson({\n        type: 'publish',\n        topic,\n        payload\n      })\n    )\n})\n\nconst room = joinRoom(\n  {appId: 'my-app-id', relayConfig: {urls: ['wss://my-relay.example']}},\n  'my-room-id'\n)\n```\n\n`createTopicStrategy` uses a short startup announcement burst followed by a\n60-second steady interval and repeats the burst after a peer drops. Set\n`reannounceOnDisconnect: false` when the signaling backend stores peer presence\nrather than delivering announcements as transient events.\n\nFor non-pub/sub signaling protocols, such as trackers that exchange offers in\nbulk, `createStrategy` is available as a lower-level advanced API.\n\n### Supabase setup\n\nTo use the Supabase strategy:\n\n1. Create a [Supabase](https://supabase.com) project or use an existing one\n2. On the dashboard, go to Project Settings -\u003e API\n3. Copy the Project URL and set that as the `appId` in the Trystero config, copy\n   the `anon public` API key and set it as `relayConfig.supabaseKey` in the\n   Trystero config\n\n### Firebase setup\n\nIf you want to use the Firebase strategy and don't have an existing project:\n\n1. Create a [Firebase](https://firebase.google.com/) project\n2. Create a new Realtime Database\n3. Copy the `databaseURL` and use it as the `appId` in your Trystero config\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n  Optional: configure the database with security rules to limit activity:\n  \u003c/summary\u003e\n\n```json\n{\n  \"rules\": {\n    \".read\": false,\n    \".write\": false,\n    \"__trystero__\": {\n      \".read\": false,\n      \".write\": false,\n      \"$room_id\": {\n        \".read\": true,\n        \".write\": true\n      }\n    }\n  }\n}\n```\n\nThese rules ensure room peer presence is only readable if the room namespace is\nknown ahead of time.\n\n\u003c/details\u003e\n\n## API\n\n### `joinRoom(config, roomId, [callbacks])`\n\nAdds local user to room whereby other peers in the same namespace will open\ncommunication channels and send events. Calling `joinRoom()` multiple times with\nthe same namespace will return the same room instance.\n\n- `config` - Configuration object containing the following keys:\n  - `appId` - **(required)** A unique string identifying your app. When using\n    Supabase, this should be set to your project URL (see\n    [Supabase setup instructions](#supabase-setup)). If using Firebase, this\n    should be the `databaseURL` from your Firebase config (also see\n    `relayConfig.firebaseApp` below for an alternative way of configuring the\n    Firebase strategy).\n\n  - `password` - **(optional)** A string to encrypt session descriptions via\n    AES-GCM as they are passed through the peering medium. If not set, session\n    descriptions will be encrypted with a key derived from the app ID and room\n    name. A custom password must match between any peers in the room for them to\n    connect. See [encryption](#encryption) for more details.\n\n  - `maxReceiveBytes` - **(optional)** Maximum payload size and budget for\n    concurrent large incoming transfers per room, in bytes. Defaults to\n    `256 * 1024 ** 2` (256 MiB). Must be a positive integer. Raise it for larger\n    files or lower it to reduce memory use.\n\n  - `passive` - **(optional)** Boolean for backup or relay peers that should\n    listen for active peers without announcing themselves while a room is\n    dormant. Passive peers activate only after hearing a non-passive peer and\n    include a passive flag in their signaling so passive peers do not connect to\n    each other. For BitTorrent, dormant passive rooms announce as seeders\n    (`left: 0`) without offers, which avoids passive-to-passive discovery while\n    keeping tracker load low.\n\n  - `relayConfig` - **(optional unless required by your chosen strategy)**\n    Object containing strategy-specific relay settings:\n    - `urls` - **(optional for 🌊 BitTorrent, 🐦 Nostr, 📡 MQTT; required for 🔌\n      WebSocket relay)** Custom list of URLs for the strategy to use to\n      bootstrap P2P connections. These would be BitTorrent trackers, Nostr\n      relays, MQTT brokers, and WebSocket relays, respectively. They must\n      support secure WebSocket connections.\n\n    - `redundancy` - **(optional, 🌊 BitTorrent, 🐦 Nostr, 📡 MQTT only)**\n      Integer specifying how many default relay endpoints to connect to\n      simultaneously. Passing a `urls` option will cause this option to be\n      ignored as the entire list will be used.\n\n    - `manualReconnection` - **(optional, 🐦 Nostr and 🌊 BitTorrent only)**\n      Boolean (default: `false`) that when set to `true` disables automatically\n      pausing and resuming reconnection attempts when the browser goes offline\n      and comes back online. This is useful if you want to manage this behavior\n      yourself.\n\n    - `warnOnRelayFailure` - **(optional)** Boolean (default: `true`) that when\n      set to `false` suppresses warnings caused by relay failures. This includes\n      failed announce attempts and strategy-specific failure messages from\n      relays or trackers.\n\n    - `supabaseKey` - **(required, ⚡️ Supabase only)** Your Supabase project's\n      `anon public` API key.\n\n    - `firebaseApp` - **(optional, 🔥 Firebase only)** You can pass an already\n      initialized Firebase app instance instead of an `appId`. Normally Trystero\n      will initialize a Firebase app based on the `appId` but this will fail if\n      you've already initialized it for use elsewhere.\n\n    - `firebasePath` - **(optional, 🔥 Firebase only)** String specifying path\n      where Trystero writes its matchmaking data in your database\n      (`'__trystero__'` by default). Changing this is useful if you want to run\n      multiple apps using the same database and don't want to worry about\n      namespace collisions.\n\n  - `rtcConfig` - **(optional)** Specifies a custom\n    [`RTCConfiguration`](https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration)\n    for all peer connections.\n\n  - `trickleIce` - **(optional)** Boolean controlling whether ICE candidates are\n    sent incrementally (`true`) or bundled with SDP (`false`). Default is\n    strategy-dependent: `true` for most strategies, `false` for BitTorrent and\n    IPFS unless explicitly set.\n\n  - `turnConfig` - **(optional)** Specifies a custom list of TURN servers to use\n    (see\n    [Troubleshooting connection issues](#troubleshooting-connection-issues)).\n    Each item in the list should correspond to an\n    [ICE server config object](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers).\n    When passing a TURN config like this, Trystero's default STUN servers will\n    also be used. To override this and use both custom STUN and TURN servers,\n    instead pass the config via the above `rtcConfig.iceServers` option as a\n    list of both STUN/TURN servers — this won't inherit Trystero's defaults.\n\n  - `rtcPolyfill` - **(optional)** Use this to pass a custom\n    [`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection)-compatible\n    constructor. This is useful for running outside of a browser, such as in\n    Node (still experimental).\n\n- `roomId` - A string to namespace peers and events within a room.\n\n- `callbacks` - **(optional)** Callback config object containing:\n  - `onJoinError(details)` - Called when room join fails due to an incorrect\n    password, when handshake admission fails (including timeout), or when peers\n    exchange SDP but WebRTC still cannot establish a direct connection. This\n    last case usually means TURN servers are needed or misconfigured (see\n    [Troubleshooting connection issues](#troubleshooting-connection-issues)).\n    `details` is an object containing `appId`, `roomId`, `peerId`, and `error`\n    describing the failure.\n\n  - `onPeerHandshake(peerId, send, receive, isInitiator)` - Async predicate that\n    runs after the transport connects but before the peer becomes active.\n    Return/resolve to accept the peer, throw/reject to deny the peer.\n    - `peerId` - ID of the pending peer.\n    - `send(data, [metadata])` - Sends handshake payloads to the pending peer.\n    - `receive()` - Resolves to the next handshake message from the peer as\n      `{data, metadata}`.\n    - `isInitiator` - Deterministic role flag for avoiding protocol deadlocks.\n\n  - `handshakeTimeoutMs` - Timeout for pending handshakes in milliseconds\n    (`10000` by default). If exceeded, the peer is denied and `onJoinError` is\n    called.\n\n  During handshake, the peer remains pending and is not included in `getPeers()`\n  and does not trigger Trystero API events (`onPeerJoin`, action receivers,\n  stream/track callbacks). Non-handshake data received while pending is dropped.\n\n  Minimal handshake example:\n\n  ```js\n  import {joinRoom} from 'trystero'\n\n  const room = joinRoom({appId: 'my-app'}, 'secure-room', {\n    onPeerHandshake: async (_, send, receive, isInitiator) =\u003e {\n      if (isInitiator) {\n        await send({challenge: 'prove-you-know-the-secret'})\n        const {data} = await receive()\n\n        if (data?.response !== 'shared-secret') {\n          throw new Error('handshake rejected')\n        }\n      } else {\n        const {data} = await receive()\n\n        if (data?.challenge !== 'prove-you-know-the-secret') {\n          throw new Error('handshake rejected')\n        }\n\n        await send({response: 'shared-secret'})\n      }\n    }\n  })\n  ```\n\nReturns an object with the following methods:\n\n- ### `leave()`\n\n  Remove local user from room and unsubscribe from room events.\n\n- ### `getPeers()`\n\n  Returns a map of\n  [`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection)s\n  for the peers present in room (not including the local user). The keys of this\n  object are the respective peers' IDs.\n\n- ### `isPassive()`\n\n  Returns whether the room was joined with `passive: true`.\n\n- ### `addStream(stream, [options])`\n\n  Broadcasts media stream to other peers.\n  - `stream` - A `MediaStream` with audio and/or video to send to peers in the\n    room.\n\n  - `options.target` - **(optional)** If specified, the stream is sent only to\n    the target peer ID (string) or list of peer IDs (array). Passing `null` or\n    omitting this option sends to all peers in the room.\n\n  - `options.metadata` - **(optional)** Additional metadata (any serializable\n    type) to be sent with the stream. This is useful when sending multiple\n    streams so recipients know which is which (e.g. a webcam versus a screen\n    capture).\n\n- ### `removeStream(stream, [options])`\n\n  Stops sending previously sent media stream to other peers.\n  - `stream` - A previously sent `MediaStream` to stop sending.\n\n  - `options.target` - **(optional)** If specified, the stream is removed only\n    from the target peer ID (string) or list of peer IDs (array).\n\n- ### `addTrack(track, stream, [options])`\n\n  Adds a new media track to a stream.\n  - `track` - A `MediaStreamTrack` to add to an existing stream.\n\n  - `stream` - The target `MediaStream` to attach the new track to.\n\n  - `options.target` - **(optional)** If specified, the track is sent only to\n    the target peer ID (string) or list of peer IDs (array).\n\n  - `options.metadata` - **(optional)** Additional metadata (any serializable\n    type) to be sent with the track. See `metadata` notes for `addStream()`\n    above for more details.\n\n- ### `removeTrack(track, [options])`\n\n  Removes a media track.\n  - `track` - The `MediaStreamTrack` to remove.\n\n  - `options.target` - **(optional)** If specified, the track is removed only\n    from the target peer ID (string) or list of peer IDs (array).\n\n- ### `replaceTrack(oldTrack, newTrack, [options])`\n\n  Replaces a media track with a new one.\n  - `oldTrack` - The `MediaStreamTrack` to remove.\n\n  - `newTrack` - A `MediaStreamTrack` to attach.\n\n  - `options.target` - **(optional)** If specified, the track is replaced only\n    for the target peer ID (string) or list of peer IDs (array).\n\n  - `options.metadata` - **(optional)** Additional metadata (any serializable\n    type) to be sent with the replacement track.\n\n- ### `onPeerJoin`\n\n  A callback property that will be called when a peer joins the room. Assigning\n  a new function replaces the previous handler; assigning `null` clears it.\n  Existing active peers are immediately replayed to a newly assigned handler.\n  - `callback(peerId)` - Function to run whenever a peer joins, called with the\n    peer's ID.\n\n  Example:\n\n  ```js\n  room.onPeerJoin = peerId =\u003e console.log(`${peerId} joined`)\n  ```\n\n- ### `onPeerLeave`\n\n  A callback property that will be called when a peer leaves the room. Assigning\n  a new function replaces the previous handler; assigning `null` clears it.\n  - `callback(peerId)` - Function to run whenever a peer leaves, called with the\n    peer's ID.\n\n  Example:\n\n  ```js\n  room.onPeerLeave = peerId =\u003e console.log(`${peerId} left`)\n  ```\n\n- ### `onPeerStream`\n\n  A callback property that will be called when a peer sends a media stream.\n  Assigning a new function replaces the previous handler; assigning `null`\n  clears it.\n  - `callback(stream, peerId, metadata)` - Function to run whenever a peer sends\n    a media stream, called with the the peer's stream, ID, and optional metadata\n    (see `addStream()` above for details).\n\n  Example:\n\n  ```js\n  room.onPeerStream = (stream, peerId) =\u003e\n    console.log(`got stream from ${peerId}`, stream)\n  ```\n\n- ### `onPeerTrack`\n\n  A callback property that will be called when a peer sends a media track.\n  Assigning a new function replaces the previous handler; assigning `null`\n  clears it.\n  - `callback(track, stream, peerId, metadata)` - Function to run whenever a\n    peer sends a media track, called with the the peer's track, attached stream,\n    ID, and optional metadata (see `addTrack()` above for details).\n\n  Example:\n\n  ```js\n  room.onPeerTrack = (track, stream, peerId) =\u003e\n    console.log(`got track from ${peerId}`, track)\n  ```\n\n- ### `makeAction(actionId, [config])`\n\n  Listen for and send custom data actions.\n  - `actionId` - A string to register this action consistently among all peers.\n\n  If `config.kind` is omitted, the action is a one-way message action. Passing\n  `kind: 'request'` creates a request/response action.\n\n  Message actions expose:\n  - `send(data, [options])` - Sends data to peers and resolves when local\n    sending is complete. To cancel an in-progress send, pass an\n    `AbortController`'s `signal` in the options (e.g.\n    `send(data, {signal: controller.signal})`) and call `controller.abort()`.\n  - `onMessage` - Nullable callback property for received messages.\n  - `onReceiveProgress` - Nullable callback property for inbound progress.\n\n  Request actions expose:\n  - `request(data, options)` - Sends to one peer and resolves with its response.\n  - `requestMany(data, options)` - Sends to many peers and resolves with\n    peer-labeled settled results.\n  - `onRequest` - Nullable callback property that returns the response.\n  - `onReceiveProgress` - Nullable callback property for inbound progress.\n\n  Both kinds expose `onReceive`, a nullable callback accepting\n  `{byteLength, peerId, metadata, kind, signal}` and returning a boolean or a\n  promise of a boolean. It can also be provided in `config`. `kind` is\n  `'message'`, `'request'`, or `'response'`; request actions apply it to both\n  requests and responses. Responses have no metadata. See\n  [receiving files](#receiving-files).\n\n  Send options use `target`, `metadata`, `onProgress`, and `signal`. Request\n  options use `target`, `metadata`, `timeoutMs`, `onProgress`, and `signal`.\n  `requestMany()` uses `targets` plus optional `onResult` for each peer result\n  as it arrives. Request timeouts cover approval, sending, and the response.\n\n  Example:\n\n  ```js\n  const cursor = room.makeAction('cursormove')\n\n  window.addEventListener('mousemove', e =\u003e cursor.send([e.clientX, e.clientY]))\n\n  cursor.onMessage = ([x, y], {peerId}) =\u003e {\n    const peerCursor = cursorMap[peerId]\n    peerCursor.style.left = x + 'px'\n    peerCursor.style.top = y + 'px'\n  }\n  ```\n\n- ### `ping(peerId)`\n\n  Takes a peer ID and returns a promise that resolves to the milliseconds the\n  round-trip to that peer took. Use this for measuring latency.\n  - `peerId` - Peer ID string of the target peer.\n\n  Example:\n\n  ```js\n  // log round-trip time every 2 seconds\n  room.onPeerJoin = peerId =\u003e\n    setInterval(\n      async () =\u003e console.log(`took ${await room.ping(peerId)}ms`),\n      2000\n    )\n  ```\n\n### `selfId`\n\nA unique ID string other peers will know the local user as globally across\nrooms.\n\n### `getRelaySockets()`\n\n**(🌊 BitTorrent, 🐦 Nostr, 📡 MQTT, 🔌 WebSocket relay only)** Returns an\nobject of relay URL keys mapped to their WebSocket connections. This can be\nuseful for determining the state of the user's connection to the relays and\nhandling any connection failures.\n\nExample:\n\n```js\nimport {getRelaySockets} from '@trystero-p2p/torrent'\n\nconsole.log(getRelaySockets())\n// =\u003e Object {\n//  \"wss://tracker.webtorrent.dev\": WebSocket,\n//  \"wss://tracker.openwebtorrent.com\": WebSocket\n//  }\n```\n\n### `pauseRelayReconnection()`\n\n**(🐦 Nostr, 🌊 BitTorrent only)** Normally Trystero will try to automatically\nreconnect to relay sockets unless `relayConfig.manualReconnection: true` is set\nin the room config. Calling this function stops relay reconnection attempts\nuntil `resumeRelayReconnection()` is called.\n\n### `resumeRelayReconnection()`\n\n**(🐦 Nostr, 🌊 BitTorrent only)** Allows relay reconnection attempts to resume.\n(See `pauseRelayReconnection()` above).\n\n## Which strategy should I choose?\n\nBy default Trystero uses the Nostr network which is highly decentralized with\nhundreds of active relays running. This is a good choice if you're interested in\ndecentralization and high redundancy. The other decentralized strategies are\nrecommended in the order of MQTT, BitTorrent, and IPFS, based on robustness.\nThese networks have far less relay redundancy than Nostr, but you might prefer\nthem for other reasons. You can of course host your own relay server for any of\nthese strategies.\n\nFor a middleground between using public relays and self-hosting, the built-in\nSupabase and Firebase strategies are a good option.\n\n---\n\nTrystero by [Dan Motzenbecker](https://oxism.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmotz%2Ftrystero","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmotz%2Ftrystero","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmotz%2Ftrystero/lists"}