Channels

A channel is a name you wrote with a delivery class behind it, and it is the only place a message is sent or received: its own send, its own on('message'), its own counters.

import { BitskiffHost, everyone } from '@bitskiff/host';

const session = await new BitskiffHost({ key: 'mrc_pk_...' }).open();

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

buzz.on('message', (m) => console.log(m.from.id, m.data));              // only buzz reaches this
sticks.on('message', (m) => console.log(m.data));                       // only sticks does
buzz.send(everyone, 'go');

The two ends agree on the name and the delivery and on nothing else. Every other setting is this end's own: maxHz paces this sender, maxBytes is this receiver's ceiling, and neither end learns the other's.

Declaring is local and reaches no wire, so the call is the same before the handle has connected and after, and a second channel(name) hands back the object the first one made—two modules reaching for one channel hold one. Naming a different delivery the second time is refused at the call site, because two deliveries under one name are two channels. There is no default set: a session that declared nothing has no channels.

reliable and lossy

They are the two classes, not two names out of a longer list.

reliable retransmits and arrives in order. It is never paced: maxHz and latestOnly are lossy-only, and a reliable channel that names either is refused at the call site.

lossy never retransmits and never waits. maxHz is this end's own ceiling—past it a message is dropped here rather than sent late—and latestOnly drops an arrival that is behind the newest packet that sender already delivered, which is what a joystick wants and a chat line does not. A host's send to one named participant on a lossy channel is dropped: lossy goes to everyone.

What send answers

send answers immediately with one of three words, and throws for nothing the network does:

It answersWhat happened
senthanded to the transport
queuedreliable, during a reconnect—it goes when the link is back
droppedover maxBytes, past this channel's pace, or one named participant on a lossy channel
import { BitskiffHost, everyone } from '@bitskiff/host';

const session = await new BitskiffHost({ key: 'mrc_pk_...' }).open();
const readout = session.channel('readout');

if (readout.send(everyone, 'you are on') === 'dropped') {
  // this one is gone; there is nothing to await and 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—and anything that has to match the two up is a field in a payload you wrote.

A host's to is one participant id, a list of them, or everyone:

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

const session = await new BitskiffHost({ key: 'mrc_pk_...' }).open();
const readout = session.channel('readout');
const ids = [...session.participants.keys()];

readout.send(ids.slice(0, 2), 'your turn');

Strings and bytes

A message is a string or bytes, the way a WebSocket frame is. When both ends of your pair want a binary format, encode it at the call site and send the Uint8Array:

import { BitskiffHost, everyone } from '@bitskiff/host';

const session = await new BitskiffHost({ key: 'mrc_pk_...' }).open();
const sticks = session.channel('sticks', { delivery: 'lossy' });

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);
sticks.send(everyone, new Uint8Array(frame.buffer));

sticks.on('message', (m) => {
  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 the two apart the way it would on a WebSocket, and a string is measured as UTF-8 bytes rather than by its length in characters.

Limits

RuleValuePast it
maxBytes, one message14080 bytes reliable, 1152 lossydropped on send, and dropped before any handler on receipt
the lossy class's bucket, per sender120 a second across every lossy channel of one enddropped
the project's bytes a second, per participant per direction16 KiB/s by default, on session.state.limits.bytesPerSecond and raised in the consoledropped locally, and a repeat offender is removed

The third row is the one to design against. The first two are the wire's ceiling.

Counters and diagnostics

Every channel carries monotonic counters—sent, received, dropped, bytesSent, bytesReceived—read them when you paint rather than on an event.

Every drop is also reported on session.on('diagnostic') and client.on('diagnostic'), with the channel's name beside the reason: tooLarge, rateLimited, bufferPressure, and unknownChannel for a message that arrived for a name this end never declared. That last one is what to read when your two halves disagree about a spelling: the message is dropped before any handler, and the sender is told nothing, because nothing on the wire refused it.

Pacing a producer

A channel reports its class's send buffer as pressure, 0 or 1. A producer reads it in the loop it already has and skips a frame rather than handing the SDK one it will drop:

import { BitskiffHost, everyone } from '@bitskiff/host';

const session = await new BitskiffHost({ key: 'mrc_pk_...' }).open();
const frames = session.channel('frames', { delivery: 'lossy' });

function onFrame(bytes: Uint8Array) {
  if (frames.pressure === 1) return;            // the link is behind; this frame is the one to lose
  frames.send(everyone, bytes);
}