# Channels

A channel is a name you choose. Every message travels on one. Each channel has its own `send`, its
own `on('message')` and its own counters.

```js
import { BitskiffHost } from '@bitskiff/host';

const host = new BitskiffHost({ key: 'bsk_pk_...' });

// Declare.
const buzz = host.channel('buzz');                                      // reliable by default
const sticks = host.channel('sticks', { delivery: 'lossy', maxHz: 60, latestOnly: true });

// Listen.
buzz.on('message', (m) => console.log(m.client.id, m.data));            // only buzz reaches this
sticks.on('message', (m) => console.log(m.data));                       // only sticks does

// Open.
const session = await host.open();
session.on('clientJoined', () => buzz.broadcast('go'));                 // every client on `buzz`
                                                                        // that is listening now
```

```ts
import { BitskiffHost, type Message } from '@bitskiff/host';

const host = new BitskiffHost({ key: 'bsk_pk_...' });

// Declare.
const buzz = host.channel('buzz');                                      // reliable by default
const sticks = host.channel('sticks', { delivery: 'lossy', maxHz: 60, latestOnly: true });

// Listen.
buzz.on('message', (m: Message) => console.log(m.client.id, m.data));   // only buzz reaches this
sticks.on('message', (m: Message) => console.log(m.data));              // only sticks does

// Open.
const session = await host.open();
session.on('clientJoined', () => buzz.broadcast('go'));                 // every client on `buzz`
                                                                        // that is listening now
```

Every block below carries on from that `host` and `session`. Each one shows the lines its section
is about; the complete programs are the [quickstart](/docs/quickstart/) and the worked examples.

A host has two send verbs. `broadcast(data)` reaches every client that named the channel.
`send(client, data)` reaches the one you name, and it takes an array to name a few. Every send
names its audience: the channel, or the clients. A client has one verb, `send(data)`, and it
speaks only to its host.

## Declarations

**The host declares every channel before it opens; a client names the ones it wants.** The spec
the host passes becomes the channel's row, and the row is the one definition. The service clamps
it and every client is handed the effective row when it joins.

`open()` seals the table. After it, `host.channel()` throws a `validation` `BitskiffError` naming
`channel`, so a session has the channels its host declared at open, for its whole life. A second
`host.channel(name)` with no spec, or the same spec, hands back the first object. A different spec
is refused: two rows under one name are two channels.

There are no default channels, and a host declares at most 64. After the open, `session.channel(name)` is the lookup, one session's view.

**Listen before `open()` too.** A host that resumes rejoins clients that can send at once, so a
handler attached after `resume()` resolves can miss their first messages.

**On a client page, name channels before `join`.** `client.channel(name)` takes a name and nothing
else. A name held before the join travels in the client's first message, so a greeting the host
sends on it lands. A host unicast on a name the client has not named answers
`{ dropped: 'notListening' }`.

Once the join is answered, `channel.declared` says whether the host
declared the name and `client.channels` is the host's table. A name the host lacks is a quiet
state: its sends answer `{ dropped: 'notDeclared' }`.

Attach `on('message')` before the join too. A message goes to the handlers on the channel at that
moment and to nobody else. It is not buffered, and it is not replayed when a handler arrives a tick
later.

A `broadcast` goes only to clients that named the channel. The `client.state.channels` list names
the channels one client is listening on. Read it first when your two halves disagree about a
spelling.

## Delivery modes

There are two, `reliable` and `lossy`.

**`reliable`** retransmits and arrives in order. It has no pace of its own: the `lifetime` and
`latestOnly` options require `delivery: 'lossy'`, and a reliable channel that names either is a
`validation` throw at the call site. Its `maxHz` defaults to 20 and paces nothing: the service only
reserves it. The service's own rate still holds it, which [Limits](#limits) below has.

**`lossy`** never retransmits and never waits. The `maxHz` option paces the sender, 30 a second
unless the host sets it: past it a message is dropped rather than sent late. The `latestOnly`
option drops an arrival that is behind the newest packet the same sender already delivered, which
is what a joystick wants and a chat line does not.

The `lifetime` option is how long a message is
worth delivering, a [duration string](/docs/guides/limits/#durations) like `200ms`: past it the
transport stops trying. You write `lifetime` as a duration string; `channel.lifetimeMs` reads it
back in milliseconds.

**A lossy channel has no unicast.** The lossy downlink is one packet to every client on the channel, so
`send(client, ...)` on a lossy channel throws `BitskiffError{ code: 'validation' }` naming
`channel` at the call site, the same way a reliable channel naming `latestOnly` is refused. On a lossy
channel, `broadcast` is the verb.

**`ordered`** says whether a receiver is handed a channel's messages in the order they were sent.
Omitted, it is `true` on a reliable channel and `false` on a lossy one. Every field is the host's
to set, and a client reads it off the row once the host's answer to the join has landed.

Each channel keeps its own order. Two channels have no order against each other, whatever their
delivery, so two things that must arrive in a fixed order belong on one channel.

Use a lossy channel for a stick, a pad or any other stream of state.
[Controller input](/docs/guides/controller/) shows how to pack one, how to sample it on a tick, and
what to do about a release packet that gets lost.

## The `send` result

Both `send` and `broadcast` return `'sent'` or a drop with its reason, and neither throws for a
network problem:

| Value | Which end returns it | What happened |
|---|---|---|
| `'sent'` | host, client | on its way; a reliable message during a reconnect is held and goes when the link is back |
| `{ dropped: 'rate' }` | host, client | past this channel's `maxHz`, or past your project's bytes a second |
| `{ dropped: 'size' }` | host, client | over the channel's `maxBytes` |
| `{ dropped: 'pressure' }` | host, client | the send buffer is behind, or a reliable queue is full |
| `{ dropped: 'notOpen' }` | host, client | the session is not open |
| `{ dropped: 'notDeclared' }` | client | the host never declared the name this send used, so nothing reached the wire. One `unknownChannel` diagnostic named the channel when the join was answered |
| `{ dropped: 'noClient' }` | host | nobody to send to: every client you named has left, or a `broadcast` where no client has named the channel |
| `{ dropped: 'notListening' }` | host | every client you named is live but has not named this channel. A different fact from `noClient`: the client is here, the line is not. Nothing is buffered waiting for a subscriber |
| `{ dropped: 'hostAway' }` | client | the host is not attached, so there is nobody to deliver to |
| `{ dropped: 'noGrant' }` | host, client | this end's grants do not carry what the send needs, such as a stick packet from a client with no `sendInput`. It is the same value on both ends for the same fact |

One send returns one reason: the first check that fails. The channel name is checked first, then
the message's size, then who is there to receive, then the link. So a host message over
`maxBytes`, sent to a client that never named the channel, comes back `size` and not
`notListening`, and the `tooLarge` diagnostic fires.

On a client the name comes first the same way: one the host never declared is `notDeclared` before
the message is weighed at all.

```js
const result = buzz.broadcast('go');   // 'sent' when it went out
if (result !== 'sent') {
  console.log(result.dropped);         // this one is gone; nothing to retry
}
```

```ts
import type { SendResult } from '@bitskiff/host';

const result: SendResult = buzz.broadcast('go');   // 'sent' when it went out
if (result !== 'sent') {
  console.log(result.dropped);                     // this one is gone; nothing to retry
}
```

There is no ack and no request-reply. A reply is a message on a channel of your own: ask on `ask`,
answer on `answer`. To match a reply to its request, put an id in the payload yourself.

## Recipients

A client is an argument to `send`. You do not reach the channel through a client. Pass one client,
or an array of them:

```js
session.on('clientJoined', (client) => {
  buzz.send(client, 'you are on');         // this one client
  buzz.send([...session.clients], 'go');   // the array form, naming a few
  buzz.broadcast('go');                    // whoever named the channel
});
```

```ts
import type { Client } from '@bitskiff/host';

session.on('clientJoined', (client: Client) => {
  buzz.send(client, 'you are on');         // this one client
  buzz.send([...session.clients], 'go');   // the array form, naming a few
  buzz.broadcast('go');                    // whoever named the channel
});
```

In a handler, the client that sent the message is `m.client`, so answering it is
`buzz.send(m.client, ...)` with no lookup.

## Listeners for one client

The call `on('message', handler, { from: client })` binds a handler for one client's messages on
that channel:

```js
buzz.on('message', (m) => console.log('anyone', m.client.id));   // the general rule

session.on('clientJoined', (client) => {
  buzz.on('message', (m) => {                  // this one client's messages, and only those
    console.log('this one', m.data);
    m.stopPropagation();
  }, { from: client });
});
```

```ts
import type { Client, Message } from '@bitskiff/host';

buzz.on('message', (m: Message) => console.log('anyone', m.client.id));   // the general rule

session.on('clientJoined', (client: Client) => {
  buzz.on('message', (m: Message) => {         // this one client's messages, and only those
    console.log('this one', m.data);
    m.stopPropagation();
  }, { from: client });
});
```

**The filtered handler runs first, and `m.stopPropagation()` stops the unfiltered ones.** It is the
rule the DOM uses: specific before general, and the specific one may answer for the whole event. It
never touches another client's handlers.

## Listeners

Every `on` returns an `Unsubscribe`, a function that removes that one listener, and calling it
twice is calling it once. That returned `Unsubscribe` is what to keep; `off(event, handler)` exists
too, for a handler you already hold.

```js
const unsubscribe = buzz.on('message', (m) => console.log(m.data));
unsubscribe();                           // this listener is gone

const page = new AbortController();      // or close a whole group at once
buzz.on('message', (m) => console.log(m.data), { signal: page.signal });
sticks.on('message', (m) => console.log(m.data), { signal: page.signal });
page.abort();                            // both are gone
```

```ts
import type { Message, Unsubscribe } from '@bitskiff/host';

const unsubscribe: Unsubscribe = buzz.on('message', (m: Message) => console.log(m.data));
unsubscribe();                           // this listener is gone

const page = new AbortController();      // or close a whole group at once
buzz.on('message', (m: Message) => console.log(m.data), { signal: page.signal });
sticks.on('message', (m: Message) => console.log(m.data), { signal: page.signal });
page.abort();                            // both are gone
```

The SDK releases a `{ from: client }` listener when that client leaves, and every channel listener
when the session ends, so an `Unsubscribe` you never called does not leak across a session. A
listener that lives as long as your page is still yours to close.

## Strings and bytes

A message is a string or bytes, the way a WebSocket frame is. To send a binary format, encode it at
the call site and send the `Uint8Array`:

```js
const frame = new DataView(new ArrayBuffer(9));   // a struct of your own: 1 tag + 2 floats
frame.setUint8(0, 1);
frame.setFloat32(1, 0.5);                         // x
frame.setFloat32(5, -0.25);                       // y
sticks.broadcast(new Uint8Array(frame.buffer));

sticks.on('message', (m) => {
  if (typeof m.data !== 'string') console.log(m.data.byteLength);   // a Uint8Array you may keep
});
```

```ts
import type { Message } from '@bitskiff/host';

const frame = new DataView(new ArrayBuffer(9));   // a struct of your own: 1 tag + 2 floats
frame.setUint8(0, 1);
frame.setFloat32(1, 0.5);                         // x
frame.setFloat32(5, -0.25);                       // y
sticks.broadcast(new Uint8Array(frame.buffer));

sticks.on('message', (m: Message) => {
  if (typeof m.data !== 'string') console.log(m.data.byteLength);   // a Uint8Array you may keep
});
```

Pick the encoding yourself: raw structs, protobuf, CBOR, MessagePack. The SDK ships no encoder and
wraps none. A receiver tells a string from bytes the way it would on a WebSocket. A string is
measured in UTF-8 bytes, not in characters.

## Limits

Five limits apply to every message, and two to the channels themselves:

| Rule | Value | Past it |
|---|---|---|
| `payloadSize`, one message | 14080 bytes reliable, 1152 lossy | `{ dropped: 'size' }` on send, and dropped before any handler on receipt |
| `maxBytes`, this channel's own ceiling | whatever the channel declared, and the platform's number when it declares none. It lowers the ceiling for one channel and never raises it: a message over `payloadSize` is refused whatever a channel says | `{ dropped: 'size' }`, the same as the platform's own |
| the lossy bucket, per client | 120 a second across all lossy channels on one end | `{ dropped: 'rate' }` |
| the reliable rate, per sender | 20 a second across all reliable channels on one end | the receiving end drops the excess and answers `rate_limited`; nothing is dropped locally |
| the project's bytes a second, per client per direction | 8 KiB/s by default, on `session.state.limits.bytesPerSecond`. A project raises it up to 16 KiB/s with `bitskiff limits set bytesPerSecond 16384`; a raise past that is stored as a pending request. A client key may set a lower one for its own clients | dropped locally, and a repeat offender is removed |
| a channel name | 64 UTF-8 bytes, and not empty | `host.channel()` throws a `validation` `BitskiffError` at the call site |
| channels per session | 64 | the 65th `host.channel()` throws a `validation` `BitskiffError` at the call site |

A lossy send past its rate is dropped locally, on the end that made it. The call answers
`{ dropped: 'rate' }`, nothing reaches the wire, and a `rateLimited` diagnostic names the channel.
The row's `maxHz`, 30 when the host set none, paces it as well as the lossy bucket.

The receiving end holds a sender to the same 120 a second and drops what is over it, so a sender
that paces itself never reaches it.

A reliable send is never paced or dropped locally for the rate. The SDK never holds one back, no
call answers `{ dropped: 'rate' }` for the rate, and no `rateLimited` diagnostic names a reliable
channel.

The 20 a second is the receiving end's budget: a token bucket, per sender, that holds a
burst of twice the rate. Past it the receiver drops the excess and answers `rate_limited` at most
once a second, `retryable` and carrying a `retryAfterMs`.

Acknowledgements and other answers are never charged, on either side, which is what keeps the close
below reachable. A heartbeat or a ping is free at the SDK's own pace and charged past it. Every
other control frame a client sends is charged, such as its hello, a presence change or a stats
report, so a client that floods them meets the same close.

A client that stays over the rate for ten seconds in a row is closed with code 4100 and booted at
once. The host does not wait for the client to honour the close.

- The host gets `clientLeft` with `e.reason` `booted`, and nothing the client still sends reaches
  a handler.
- The client goes to `joining` and tries again after the `retryAfterMs`, ten seconds by default.
  That return is refused, and the client ends with `ended.reason` `booted`.

A host that stays over the rate is never closed for it. Closing a client is the host's own act, so
the host's excess is dropped and answered like anyone's.

A host sees that answer on the session's `error` event, as a `BitskiffError` with
`code: 'rate_limited'`, `retryable: true`, `retryAfterMs`, and `details.from`, the id of the client
that was over. The client SDK raises no event for it: it logs the error and the session carries on.
The receiving end raises nothing either; the drop is counted and logged at debug. Keep reliable
sends under the rate on both ends.

Each end has its own bucket. The host's sends are counted against the host's, and every client's
against its own.

The project's bytes-a-second limit is the one to design against. The other four are the wire's own
ceilings, and a message that stays inside them still spends bytes against yours.

## Counters and diagnostics

Every channel carries monotonic counters on `channel.counters`: `sent`, `received`, `dropped`,
`bytesSent` and `bytesReceived`. Read them when you draw a frame; they do not fire events.

The usage the platform meters counts the same messages: what your code sent and received, never
the handshake, roster, keepalive and key traffic the SDK exchanges on its own.

```js
console.log(buzz.counters.sent, buzz.counters.dropped);   // on `counters`, not on the channel
```

Four drop reasons are also reported on a `diagnostic` listener: `tooLarge`, `rateLimited`,
`bufferPressure`, or `unknownChannel`. On a host that is `session.on('diagnostic')`; on a client
page it is `client.on('diagnostic')`, on the `BitskiffClient` handle. A roster `Client` on the
host has no `diagnostic` of its own.

**`diagnostic.channel` is optional.** It carries a channel's name when a channel rule is what
dropped the message: `tooLarge` against the channel's `maxBytes`, `rateLimited` against a lossy
channel's `maxHz` or the lossy bucket, and `unknownChannel`. It is absent on `bufferPressure`,
which the transport's send buffer decided, and on a `rateLimited` the project's bytes-a-second cap
decided. Guard the field.

The `bytes` field is the payload's own bytes, the number you handed `send`. The envelope, the seal
and the engine's framing are the SDK's overhead and are not counted there. `bytesPerSecond` is the
cap in force, and it is absent when no cap named one.

```js
session.on('diagnostic', (d) => {
  console.log(d.reason, d.channel ?? '(no channel)', d.bytes);
});
```

```ts
import type { Diagnostic } from '@bitskiff/host';

session.on('diagnostic', (d: Diagnostic) => {
  console.log(d.reason, d.channel ?? '(no channel)', d.bytes);
});
```

Five more reasons arrive on the same listener when the session runs encrypted. They are listed
under [Encryption](#encryption) below.

One more reason reaches this listener from the host's own side. `peerBooted` fires once, just
before the host boots a client that stayed over the `bytesPerSecond` limit above for ten seconds
in a row, or that kept sending malformed envelopes. Its `detail` carries the client's id and the
limit it broke, or the close code.

The host's logger writes the same line. The ordinary boot
follows, and that is what the client sees.

The `unknownChannel` reason is a **sender-side** diagnostic: it fires once on the client that
named a channel the host never declared, and the message never reaches the
wire. Read it when the two halves disagree about a spelling.

It never fires on receipt: a broadcast on a name
this end knows but never named is dropped before any handler and counted, with no diagnostic,
because a broadcast reaches every client on the channel and not a subscription list. The rule is
that the sender is told once, on the line that named the channel, and nowhere else.

## Encryption

A session can run end-to-end encrypted. The host mints its own identity key at `open`, every
payload either way is sealed under a group key the host owns, and nothing in between can read what
you send.

Two switches turn it on. The project's setting is `off` or `required`. The host's constructor
option is `e2ee: 'required'`, and leaving the option out is off. There is no third value and no
negotiation.

The project's switch is the one the service holds every session to. Set it from the command line
with `bitskiff settings set e2ee required`; the console's Settings panel shows it and does not
write it. The host's own switch is `e2ee: 'required'` on the constructor:

```js
import { BitskiffHost } from '@bitskiff/host';

const session = await new BitskiffHost({ key: 'bsk_pk_...', e2ee: 'required' }).open();
```

```ts
import { BitskiffHost, type Session } from '@bitskiff/host';

const session: Session = await new BitskiffHost({ key: 'bsk_pk_...', e2ee: 'required' }).open();
```

A host opened plain against a `required` project is refused, and so is a `required` host against a
project that is `off`. Either way `open()` rejects with `BitskiffError{ code: 'validation' }` and
`details.option` of `e2ee`, before the transport attaches, so set the two to the same value.

Under encryption the join code carries the host's key fingerprint after a `.`, which is what binds
a client to this host and not another. A client whose runtime lacks the primitives is refused
`e2ee_unsupported`; one joining from a stored entry that carries no fingerprint is refused
`e2ee_required`. Both are answered before a place is reserved.

Each client's own standing is `client.state.e2ee`, an `E2eeInfo`: `bound` is true once that
client's key is bound and its first message verified, `generation` is the group-key generation it
holds, and `confirmation` is six digits to read aloud against the other end's, absent until
`bound`.

A client that has not confirmed yet has no `e2ee` object at all. A rekey the client
already holds a wrap for moves `generation` and arrives as a `clientChanged` with `change: 'e2ee'`.

Five `diagnostic` reasons belong to encryption, and each one is a message this end refused to
deliver:

| `reason` | What happened |
|---|---|
| `e2eeSignature` | the message's signature did not verify |
| `e2eeTag` | its authentication tag failed |
| `e2eeReplay` | it repeated a sequence number already seen |
| `e2eeGeneration` | it was encrypted under a key generation this end no longer holds |
| `e2eePlaintext` | it arrived unencrypted on an encrypted session |

None of the five is a hiccup to retry through. Treat any of them as a wire to distrust: log it with
the client it came from, and end that client rather than carrying on.

## Congestion

A channel reports its send buffer as `congested`, a boolean. Read it in the loop you already have
and skip a frame rather than hand the SDK one it will drop:

```js
function onFrame(bytes) {                // `bytes` is one encoded frame, a Uint8Array
  if (sticks.congested) return;          // the link is behind; this frame is the one to lose
  sticks.broadcast(bytes);
}
```

```ts
function onFrame(bytes: Uint8Array): void {
  if (sticks.congested) return;          // the link is behind; this frame is the one to lose
  sticks.broadcast(bytes);
}
```

## Next steps

- [Controller input](/docs/guides/controller/)
- [Lifecycle](/docs/guides/lifecycle/)
- [Client pages](/docs/guides/client-page/)
- Worked examples: [cat and mouse](/docs/guides/examples/cat-and-mouse/),
  [a queue and four slots](/docs/guides/examples/queue-and-slots/), and
  [a poll with a reveal](/docs/guides/examples/poll-and-reveal/)
