# 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](/docs/guides/limits/#raises)). 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](/docs/quickstart/) page:
host blocks in the host page, client blocks in the client page.

The host declares `peer` before it opens the session.

```js
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);
});
```

```ts
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);
});
```

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](/docs/guides/node-hosts/#peer-to-peer-connections) 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](/docs/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.

```js
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);
```

```ts
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);
```

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.

```js
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
});
```

```ts
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
});
```

`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](/docs/guides/limits/#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:

```js
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');
```

```ts
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');
```

`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'` |
