Transports
Turn on peer for lower latency. A client's data then takes a direct connection to the host.
There a channel can run at its linkMaxHz, a higher rate your plan allows (see
channel rates). Use allowed: false for people you do not trust.
Leave peer off on a Node host with no WebRTC library to give it.
Setup
Each block goes inside the <script type="module"> of a quickstart page:
host blocks in the host page, client blocks in the client page.
The host declares peer before it opens the session.
import { BitskiffHost, peer, type ClientChanged } from '@bitskiff/host';
const host = new BitskiffHost({ key: 'bsk_pk_...' }); // your host key
host.transport(peer()); // peer for every client
const session = await host.open();
session.on('clientChanged', (e: ClientChanged) => {
if (e.change === 'transport') console.log(e.client.id, e.client.state.transports.peer.status);
});
import { BitskiffHost, peer } from '@bitskiff/host';
const host = new BitskiffHost({ key: 'bsk_pk_...' }); // your host key
host.transport(peer()); // peer for every client
const session = await host.open();
session.on('clientChanged', (e) => {
if (e.change === 'transport') console.log(e.client.id, e.client.state.transports.peer.status);
});
The host reads each client's peer record in client.state.transports.peer. The block under
Public venues carries on from this host.
How a hop runs
A transport is what carries a client's data. Every session runs on the server connection, which
links each end to Bitskiff's servers for the whole session. Its record is server: client.server
on the client page, client.state.transports.server on the host. A client that declares peer
also has its record, off until the host turns it on. A hop moves all of a client's channels from
one transport to another.
join()resolves on the server connection.peerreadsavailableonce the host has it on and the client has joined.- The person taps a button, and the page calls
connect(). connect()resolvesconnected, and the client's data now ridespeer.disconnect()moves it back to the server connection.
peer is direct only. When the two ends cannot reach each other, it fails and the client stays on
the server connection. Declare relay() beside it for a link through a relay: a TURN server that
forwards the traffic, so neither end learns the other's address. Bitskiff runs one for you.
Node hosts
A Node host has no WebRTC of its own, so pass a peer connection in peer({ peerConnection }). With
werift, that is host.transport(peer({ peerConnection: (config) => new RTCPeerConnection(config) })).
Without it, every client's peer reads unavailable with reason notAttached.
Node hosts shows the install and the import.
Client connection
The sample client page moves onto peer by itself once it reads available. A client page of
your own joins with a client key, declares peer and calls connect() itself, as below. The
quickstart shows how that page joins.
Your page shows a notice and a button. The SDK never connects by itself: the page calls
connect() when the person taps.
import { BitskiffClient, peer } from '@bitskiff/client';
const client = new BitskiffClient({ key: 'bsk_pk_...' }); // your client key
const code = 'ABCD'; // the join code the person typed
const notice = document.createElement('p');
notice.textContent = 'Connecting directly shares your network address with this host.';
const button = document.createElement('button');
button.textContent = 'Connect directly';
const stop = document.createElement('button');
stop.textContent = 'Disconnect';
const why = document.createElement('p');
document.body.append(notice, button, stop, why);
const viaPeer = client.transport(peer()); // declared before join
const paint = (): void => {
button.hidden = viaPeer.status !== 'available';
notice.hidden = button.hidden;
stop.hidden = viaPeer.status !== 'active';
if (viaPeer.reason === 'notAllowed') why.textContent = 'The host did not allow a direct connection.';
else if (viaPeer.reason === 'limit') why.textContent = 'The host has no direct connection free.';
else if (viaPeer.reason !== 'failed') why.textContent = '';
else if (viaPeer.nextAt === undefined) why.textContent = 'Off until the host reloads.';
else why.textContent = `Try again in ${Math.ceil((viaPeer.nextAt - client.now()) / 1000)} s.`;
};
paint(); viaPeer.on('status', paint); // read once, then follow changes
button.onclick = () => viaPeer.connect(); // resolves 'connected' or 'failed'
stop.onclick = () => viaPeer.disconnect(); // back to the server connection
await client.join(code);
import { BitskiffClient, peer } from '@bitskiff/client';
const client = new BitskiffClient({ key: 'bsk_pk_...' }); // your client key
const code = 'ABCD'; // the join code the person typed
const notice = document.createElement('p');
notice.textContent = 'Connecting directly shares your network address with this host.';
const button = document.createElement('button');
button.textContent = 'Connect directly';
const stop = document.createElement('button');
stop.textContent = 'Disconnect';
const why = document.createElement('p');
document.body.append(notice, button, stop, why);
const viaPeer = client.transport(peer()); // declared before join
const paint = () => {
button.hidden = viaPeer.status !== 'available';
notice.hidden = button.hidden;
stop.hidden = viaPeer.status !== 'active';
if (viaPeer.reason === 'notAllowed') why.textContent = 'The host did not allow a direct connection.';
else if (viaPeer.reason === 'limit') why.textContent = 'The host has no direct connection free.';
else if (viaPeer.reason !== 'failed') why.textContent = '';
else if (viaPeer.nextAt === undefined) why.textContent = 'Off until the host reloads.';
else why.textContent = `Try again in ${Math.ceil((viaPeer.nextAt - client.now()) / 1000)} s.`;
};
paint(); viaPeer.on('status', paint); // read once, then follow changes
button.onclick = () => viaPeer.connect(); // resolves 'connected' or 'failed'
stop.onclick = () => viaPeer.disconnect(); // back to the server connection
await client.join(code);
The page must show the notice before it calls connect(). No address leaves the page until then.
connect() never throws. Called while unavailable, it resolves failed and leaves reason and
nextAt unchanged. Called while active, it resolves connected. Called while connecting, it
resolves the same as the call in flight.
Fallback
A hop is make-before-break: the new connection is up before the old one closes. A peer
connection that drops has no warning. The SDK moves the client's data back to the server connection
on its own. Lossy messages from about the last second can be lost. Reliable messages are sent again
on the server connection, in order, and none are lost.
Channels
Reliable stays reliable on every transport, and lossy stays lossy. On a transport with no lossy path, lossy messages ride the reliable path, and stale ones are still dropped. Your sends, listeners and channels do not change.
Status
A peer record's status is one of four values.
| Status | Meaning |
|---|---|
active | the client's data rides peer |
available | the page may call connect() now |
connecting | a hop is being set up |
unavailable | the page cannot connect now; reason says why |
reason is set only while peer is unavailable.
| Reason | Meaning |
|---|---|
notAttached | the client has not joined, the host has peer off, or this device cannot make a peer connection |
notAllowed | the host has not allowed this client, or has denied it |
limit | the host already holds 16 peer connections, or its maxConnections |
failed | the last try or the live connection failed |
reason is left unset while the client or the host is away, or while the client's server
connection is down. Presence covers those cases.
To learn why a connection closed or was never offered, listen for the diagnostic event. It
carries directClosed with the close reason, or directNotOffered with its cause.
After failed, nextAt says when peer reads available again, 30 seconds later. It is a
client.now() instant, so the seconds left are Math.ceil((viaPeer.nextAt - client.now()) / 1000).
A failed with no nextAt means the host closed the connection because this client sent too much.
It stays failed until the host reloads. The page above shows the text Off until the host reloads. for it.
Presence
A client is away while its page is hidden, for example when the person switches apps. An away
client's peer connection closes, and its data goes back to the server connection. When the page is
visible again, peer reads available. The page calls connect() again when the person wants it.
An active connection that carries no data for the session's reconnectBudget closes, and the data
goes back to the server connection. A hidden host page closes every peer connection and offers
none until it is visible again.
Public venues
peer() lets every client connect, so use it for people you trust. A public venue declares
peer({ allowed: false }) and allows each client it trusts. This block replaces the
host.transport and open under Setup, so the host still opens one session.
import type { Client } from '@bitskiff/host';
const trustedPeer = host.transport(peer({ allowed: false })); // peer for no client yet
const venue = await host.open();
const trusted = new Set(['client-id']); // client ids your app trusts
venue.on('clientJoined', (client: Client) => {
if (trusted.has(client.id)) client.allow(trustedPeer); // this client may connect
});
const trustedPeer = host.transport(peer({ allowed: false })); // peer for no client yet
const venue = await host.open();
const trusted = new Set(['client-id']); // client ids your app trusts
venue.on('clientJoined', (client) => {
if (trusted.has(client.id)) client.allow(trustedPeer); // this client may connect
});
client.deny(trustedPeer) takes it back, and a live connection falls back to the server
connection. Both last while the client stays in the session, a host reload included.
Network changes
When a device changes network, the SDK restarts ICE on the live peer connection. ICE is the WebRTC
step that finds a route between the two ends. peer reads failed only if the restart takes longer
than 750 ms.
A change can take the server connection too, such as a Wi-Fi roam. The peer connection then
closes. Messages the app sends wait until the server connection is back.
Options
The host passes these in host.transport(peer({ ... })).
| Option | Type | Default | Meaning |
|---|---|---|---|
allowed | boolean | true | false turns peer on for no client until the host calls allow for it |
peerConnection | function | none | Node only: (config) => new RTCPeerConnection(config) |
maxConnections | integer, 1 to 16 | 16 | the most peer connections this host holds across its sessions |
peer takes Bitskiff's STUN servers from the offer, and takes no servers of your own. A link that
must hide each end's address is relay(), declared at both ends beside or instead of peer().
A client page can hide its own address with client.transport(relay()) in place of peer().
Bitskiff meters relay traffic, and client-minutes count the same on any transport. Prices are not
set yet, and today every price is zero. Relay bytes have a monthly limit, turnBytesPerMonth:
past it, no new relay credential is issued, while joins and live connections continue. See
Quotas.
Migration from direct
An upgraded client page stays on the server connection. Under
direct, a client page accepted the link by itself. Now it hops only when it declarespeerand callsconnect(). Add the notice and button:
import { BitskiffClient, peer } from '@bitskiff/client';
const client = new BitskiffClient({ key: 'bsk_pk_...' }); // your client key
const viaPeer = client.transport(peer()); // declared before join
const notice = document.createElement('p');
notice.textContent = 'Connecting directly shares your network address with this host.';
const button = document.createElement('button');
button.textContent = 'Connect directly';
document.body.append(notice, button);
const paint = (): void => {
button.hidden = viaPeer.status !== 'available';
notice.hidden = button.hidden;
};
paint(); viaPeer.on('status', paint);
button.onclick = () => viaPeer.connect();
await client.join('ABCD');
import { BitskiffClient, peer } from '@bitskiff/client';
const client = new BitskiffClient({ key: 'bsk_pk_...' }); // your client key
const viaPeer = client.transport(peer()); // declared before join
const notice = document.createElement('p');
notice.textContent = 'Connecting directly shares your network address with this host.';
const button = document.createElement('button');
button.textContent = 'Connect directly';
document.body.append(notice, button);
const paint = () => {
button.hidden = viaPeer.status !== 'available';
notice.hidden = button.hidden;
};
paint(); viaPeer.on('status', paint);
button.onclick = () => viaPeer.connect();
await client.join('ABCD');
direct is gone: passing it throws validation naming the option.
On the host:
| Before | Now |
|---|---|
host.open({ direct: true }) | host.transport(peer()) before host.open() |
direct: { iceServers } | remove it: the relay is Bitskiff's |
direct: { iceTransportPolicy: 'relay' } | host.transport(relay()) |
direct: { peerConnection } | peer({ peerConnection }) |
client.state.transport.direct | client.state.transports.peer.status === 'active' |
client.state.transport.path, client.state.transport.rttMs | client.state.transports.server.link?.path, .link?.rttMs |
On the client page:
| Before | Now |
|---|---|
direct: false | declare no peer |
direct: { iceTransportPolicy: 'relay' } | client.transport(relay()) |
direct: { iceServers } | remove it: the host's offer carries the servers |
client.state.transport.direct | viaPeer.status === 'active' |