Bitskiff

Transports

On this page

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.

  1. join() resolves on the server connection.
  2. peer reads available once the host has it on and the client has joined.
  3. The person taps a button, and the page calls connect().
  4. connect() resolves connected, and the client's data now rides peer.
  5. 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.

StatusMeaning
activethe client's data rides peer
availablethe page may call connect() now
connectinga hop is being set up
unavailablethe page cannot connect now; reason says why

reason is set only while peer is unavailable.

ReasonMeaning
notAttachedthe client has not joined, the host has peer off, or this device cannot make a peer connection
notAllowedthe host has not allowed this client, or has denied it
limitthe host already holds 16 peer connections, or its maxConnections
failedthe 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({ ... })).

OptionTypeDefaultMeaning
allowedbooleantruefalse turns peer on for no client until the host calls allow for it
peerConnectionfunctionnoneNode only: (config) => new RTCPeerConnection(config)
maxConnectionsinteger, 1 to 1616the 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 declares peer and calls connect(). 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:

BeforeNow
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.directclient.state.transports.peer.status === 'active'
client.state.transport.path, client.state.transport.rttMsclient.state.transports.server.link?.path, .link?.rttMs

On the client page:

BeforeNow
direct: falsedeclare no peer
direct: { iceTransportPolicy: 'relay' }client.transport(relay())
direct: { iceServers }remove it: the host's offer carries the servers
client.state.transport.directviaPeer.status === 'active'