Browsers
A web app running js-libp2p and a Dart or Flutter app reach each other the
same way two Dart hosts do: both book a slot on a circuit relay, one dials
the other’s /p2p-circuit/webrtc address, and WebRTC takes over from there.
This guide sets up the browser side, and covers what the relay needs.
The JavaScript here comes from the page the interop tests run in Chrome, Firefox and WebKit against dart_libp2p hosts.
The relay
Section titled “The relay”The browser and the Dart app must reach the same relay, each over a transport it has:
| Peer | Reaches the relay over |
|---|---|
| Browser | WebSockets: /wss from a page served over HTTPS, /ws only from http:// pages (browsers block ws:// from secure pages) |
| Dart or Flutter app | TCP or UDX (dart_libp2p has no WebSocket transport) |
So the relay listens on a WebSocket address, ideally with TLS, and on TCP or UDX. A go-libp2p relay can share one TCP port between raw TCP and WebSockets. For example, a go-ricochet server configured like this serves browsers and Dart apps:
transports: [udx, tcp, websocket]listen_addresses: - /ip4/0.0.0.0/udp/55223/udx - /ip4/0.0.0.0/tcp/55223 - /ip4/0.0.0.0/tcp/55223/ws - /ip4/0.0.0.0/tcp/55223/tls/wsThe relay only carries signaling, a few KB per connection, so go-libp2p’s default relay limits (2 minutes, 128 KiB per relayed connection) are plenty.
The browser node
Section titled “The browser node”Install js-libp2p with the WebSockets, WebRTC and circuit relay transports:
npm install libp2p @libp2p/websockets @libp2p/webrtc @libp2p/circuit-relay-v2 \ @chainsafe/libp2p-noise @chainsafe/libp2p-yamux @libp2p/identify @libp2p/ping \ @multiformats/multiaddrIf the relay also speaks UDX, as go-ricochet and dart_libp2p relays can,
register the UDX multiaddr codec before creating the node. The relay lists
its /udx address in the reservation it grants, and without the codec
js-libp2p rejects the whole reservation with Protocol 768 was unknown.
The browser entry of js-udx’s libp2p package (0.1.1 or later) registers it, and has no Node.js code, so it bundles for the browser:
npm install @stephanfeb/libp2p-udxThe test page imports it before anything else uses multiaddrs:
// UDX relays (go-ricochet) list their /udx address in reservations; without// the UDX codec js-libp2p fails the whole reservation ("Protocol 768 was// unknown"). This browser entry registers it.import '@stephanfeb/libp2p-udx/multiaddr'Import that entry, not the package root: the root brings in the UDX
transport, which needs Node.js. The app and the package must share one copy
of @multiformats/multiaddr (they do with js-libp2p v3; npm ls @multiformats/multiaddr shows the copies you have).
Or, without the dependency, register the codec yourself. It’s one line:
import { registry } from '@multiformats/multiaddr'
registry.addProtocol({ code: 0x0300, name: 'udx', size: 0 })Then create the node. It listens on <relay>/p2p-circuit, which books a
reservation on that relay, and on /webrtc, which accepts WebRTC
connections signaled through it:
const node = await createLibp2p({ addresses: { listen: [`${relay}/p2p-circuit`, '/webrtc'] }, transports: [webSockets(), webRTC({ rtcConfiguration }), circuitRelayTransport()], connectionEncrypters: [noise()], streamMuxers: [yamux()], // The test relay and Dart host are on loopback, which browsers' default // gater refuses. connectionGater: { denyDialMultiaddr: async () => false }, services: { identify: identify(), ping: ping() }})console.log(`browser peer ${node.peerId}`)await node.handle(ECHO, async (stream) => { for await (const chunk of stream) { if (!stream.send(chunk)) await stream.onDrain() } await stream.close()})relayis the relay’s WebSocket address with its peer ID, for example/dns4/relay.example.com/tcp/443/wss/p2p/12D3KooW….rtcConfigurationholds the browser’s ICE servers. It’sundefinedhere (js-libp2p’s defaults include public STUN servers); for TURN, see Networks.- The
connectionGateroverride is only for the tests, which run on loopback. A web app talking to public addresses doesn’t need it. ECHOis a protocol ID (/test/echo/1.0.0); the handler sends back every chunk it reads. Your own protocols go here the same way.
Browser dials the Dart app
Section titled “Browser dials the Dart app”Once the Dart app has a reservation on the relay (see
Getting started), the browser dials its WebRTC
address, <relay>/p2p-circuit/webrtc/p2p/<dart peer id>:
const connection = await node.dial(target, { signal: AbortSignal.timeout(30_000) })js-libp2p opens the relayed connection, runs the signaling protocol over
it, and returns the WebRTC connection. It reports /webrtc as the
multiplexer and native as the encryption, and it isn’t limited, even
though the relayed connection it came through is.
Streams work as on any js-libp2p connection. This helper sends a buffer on a new stream, half-closes it, and collects the reply:
/** Sends `data` on a fresh echo stream, half-closes, and returns the reply. */async function echo (connection, data) { const stream = await connection.newStream(ECHO) const reply = (async () => { const chunks = [] for await (const chunk of stream) chunks.push(chunk.subarray()) const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0)) let offset = 0 for (const c of chunks) { out.set(c, offset); offset += c.length } return out })() for (let offset = 0; offset < data.length; offset += 16 * 1024) { if (!stream.send(data.subarray(offset, offset + 16 * 1024))) await stream.onDrain() } await stream.close() return reply}On the Dart side, the incoming connection joins the host’s swarm. The host’s stream handlers serve the browser’s streams, and identify and ping answer as they would over TCP.
The Dart app dials the browser
Section titled “The Dart app dials the browser”The other direction works the same as between two Dart hosts. The browser
has a reservation on the relay, from listening on <relay>/p2p-circuit.
The Dart app connects to it through the relay, then upgrades, exactly as the
dialer in Getting started
does:
connectto the relay (over TCP or UDX).connectto the browser at<relay>/p2p-circuit.WebRtcTransport.upgrade(browserPeerId).
The browser’s /webrtc listener answers the offer, and the Dart app gets a
direct connection to the browser.
Checklist
Section titled “Checklist”- The relay listens on WSS for HTTPS pages, and on TCP or UDX for Dart.
- The page registers the UDX codec if the relay speaks UDX.
- Both sides have STUN servers: js-libp2p does by default, Dart needs them in
RtcConfig.iceServers. - For networks without a direct path, both sides have the same TURN server (see Networks).