# BYOC: the whole API on one page

BYOC turns a browser tab into a small real-time server. One tab **hosts** a
room; other tabs **connect** to it with a link. Traffic goes peer to peer
(WebRTC), falling back to TURN or an encrypted relay automatically. There is
no backend to write or deploy: the room's logic runs in the host's tab.

Use it for small groups (2–16 people) in real-time, session-length apps: games,
quizzes, whiteboards, pair editing, "join my session". Don't use it for
large audiences, for data that must outlive the host's tab, or where players
must not be able to cheat (the host runs the rules).

## Import

```js
import { host, connect } from 'https://byoc.infinitefun.com/v0/byoc.js';
```

Works in any `<script type="module">`, on any static page (https or
localhost). No API key. The default operator is
`wss://api-byoc.infinitefun.com/v1`. To use your own operator:
`host({ operators: ['wss://your-operator.example/v1'] })` (same for
`connect`), or set `window.BYOC_OPERATORS = ['wss://…/v1']` before importing.

## Pattern 1: one HTML file, host or guest

```js
import { host, connect } from 'https://byoc.infinitefun.com/v0/byoc.js';

let conn;
if (location.hash.includes('byoc=')) {
  conn = await connect();                        // guest: link is in the URL
} else {
  const room = await host();                     // host: this tab is the server
  room.onconnection = (c) => {                   // runs in the host tab for each player
    c.onmessage = (e) => room.broadcast(e.data); // the "server" logic
  };
  conn = await room.connectSelf();               // the host plays too (pattern 2)
  showShareLink(room.link);                      // e.g. put it in a text box / QR code
}
conn.onmessage = (e) => render(JSON.parse(e.data));
conn.send(JSON.stringify({ type: 'move', x: 1 }));
```

## Pattern 2: the host plays through a loopback

`await room.connectSelf()` returns a `Connection` from the host to its own
room. It shows up in `room.onconnection` like any guest. Write the client code
once and use it for both host and guests. Keep "server" code (in
`onconnection`) and "client" code (using `conn`) separate even though they
share a tab.

## Pattern 3: migrating WebSocket code

```js
// before: const ws = new WebSocket('wss://my-server/game');
const ws = await connect(link);
// ws.onmessage / ws.send / ws.close / ws.readyState / ws.onclose work the same.
```

Differences from WebSocket: `connect()` returns a promise that resolves when
the connection is already **open** (no `onopen` needed); binary data arrives
as `ArrayBuffer` (`binaryType` is always `'arraybuffer'`); messages that
arrive before you set `onmessage` are kept, not dropped.

## host(options?) → Promise<Room>

| Option | Default | Meaning |
|---|---|---|
| `name` | `'default'` | Identity name; one page can host several rooms with different names |
| `public` | `false` | No secret: anyone with the room id or short code can join |
| `max` | unlimited | Maximum connections (the loopback counts) |
| `accept({peerId, resumed, info})` | accept all | Return/resolve `false` to refuse a guest |
| `meta` | none | Small public JSON guests see before connecting |
| `storage`, `baseUrl` | IndexedDB, current page | Where the identity lives; page links point to |

The host's identity (and therefore its link) is stored in IndexedDB and
**stays the same across reloads**. Guests reconnect automatically after a host
reload.

`Room`:

| Member | Meaning |
|---|---|
| `room.link` | Full URL to share (`https://page#byoc=<id>.<secret>`) |
| `room.shortCode` | e.g. `"KX7-4PQR"`: easy to read aloud. Joins public rooms; private rooms need `{ secret }` too |
| `room.hostId`, `room.secret`, `room.address` | Parts of the link |
| `room.onconnection = (conn) => …` | A guest connected (also after resuming: check `conn.peer.resumed`) |
| `room.connections` | Open connections |
| `room.broadcast(data, { except })` | Send to everyone (optionally not to `except`) |
| `await room.connectSelf()` | Loopback connection (pattern 2) |
| `room.setMeta(obj)` | Update public info |
| `await room.rotateSecret()` | New link; old links stop working (connected guests stay) |
| `room.persist(key, value)` / `await room.restore(key)` | Save/load state in this browser (survive a host reload) |
| `room.ondiagnostic = (d) => …` | Events for status UI (see Diagnostics) |
| `room.close()` | Everyone is disconnected cleanly |

## connect(target?, options?) → Promise<Connection>

`target`: a full link, `'#byoc=…'`, `'<hostId>.<secret>'`, or a short code.
Default: the current page's URL.

| Option | Default | Meaning |
|---|---|---|
| `info` | none | JSON the host sees as `conn.peer.info` (e.g. `{ name }`) |
| `secret` | from link | Needed with a short code for a private room |
| `wait` | `0` | ms to wait for an offline host instead of failing |
| `timeout` | `30000` | Give up connecting after this long |
| `resume` | `true` | Reconnect automatically when the host reloads or the network blips |
| `resumeGrace` | `30000` | How long to keep trying before closing |
| `awayGrace` | `10000` | How long to wait for a host whose page closed or reloaded |
| `ondiagnostic` | none | Diagnostics from the very start |

Options for both `host` and `connect`: `operators`, `debug: true` (console
timeline), `ondiagnostic`, `iceServers`, `iceTransportPolicy`, `webrtc: false`
(relay only), `maxMessageSize` (default 1 MiB). On `host`, `resumeGrace`
(default 30000) is how long a dropped guest keeps its seat, and `awayGrace`
(default 5000) is how long a guest whose page closed or reloaded keeps it
(a reload that redials in time gets the full `resumeGrace`).

## Connection

| Member | Meaning |
|---|---|
| `send(string \| ArrayBuffer \| TypedArray)` | Send (objects: `JSON.stringify` first). Up to 1 MiB |
| `onmessage = (e) => e.data` | `string` or `ArrayBuffer` |
| `onclose = (e) => e.code, e.reason, e.wasClean` | Closed for good (after resume gave up, or a clean close) |
| `onopen`, `onerror`, `addEventListener` | As WebSocket |
| `close(code?, reason?)` | Codes 1000 or 3000–4999 |
| `readyState` | 0 connecting, 1 open, 2 closing, 3 closed (stays 1 while resuming) |
| `bufferedAmount` | Bytes queued |
| `peer` | `{ id, resumed, path: 'direct'\|'turn'\|'relay'\|'loopback', rtt, info }` |
| `channel(name, { reliable: false })` | Extra channel with the same API; unreliable = unordered, may drop (fast game input) |
| `ondiagnostic` | Per-connection diagnostics |

`peer.id` is stable for a guest across resumes (host reload, network change),
so use it as the player's seat key.

## Diagnostics and errors

Failures reject with a `ByocError`: `{ code, message, hint, details }`.
`message` is for people; `hint` says what to do. Codes:
`bad-link`, `operator-unreachable`, `host-offline`, `unknown-code`,
`bad-proof` (wrong/old secret), `rejected`, `full`, `rate-limited`,
`host-auth-failed` (possible tampering), `connect-timeout`, `replaced` (room
opened in another tab), `too-large`, `unsupported`.

`ondiagnostic(d)` gets `{ code, level: 'info'|'warn'|'error', message, hint }`.
Useful codes: `using-direct`, `using-turn`, `using-relay`, `upgraded`,
`host-reconnecting`, `resumed`, `host-gone`, `peer-reconnecting`, `peer-gone`,
`host-hidden` (the host switched tabs), `webrtc-disabled`, `ice-failed`.
Show `warn`/`error` messages to the user.

`import { selfTest } from '…'; const r = await selfTest();` checks the
operator, WebRTC, STUN and TURN and returns `r.summary`, a sentence to show.

## Pitfalls

- **The host must keep the tab open and visible.** Hidden tabs are throttled
  (timers slow to 1/s or 1/min). Guests get `host-hidden`; show "waiting for
  the host". Hosting from a phone in the background does not work.
- **Put the room logic in `room.onconnection`**, not in guest code. Guests
  should send intents ("move left"), the host decides and broadcasts state.
- **Late joiners need state.** In `onconnection`, send the current state to
  the new connection first.
- **Resume, not reconnect.** Don't call `connect()` again on `onclose`
  unless you want a brand-new seat; BYOC already retries for `resumeGrace`.
  Messages sent while reconnecting are queued; messages in flight during the
  drop can be lost, so resend full state after `resumed` if it matters.
- **Use `conn.peer.id` for seats**, not the Connection object (a host reload
  creates new Connection objects with the same `peer.id`).
- **Serialize objects** with `JSON.stringify`; `send({})` throws.
- **On the relay (`conn.peer.path === 'relay'`), sends are paced** to about
  180 frames/s (a frame is up to 16 KB of a message). Bursts are queued, not
  lost. Watch `conn.bufferedAmount` if you stream a lot, and prefer fewer,
  larger state updates over many tiny ones.
- The link's secret is after the `.` in `#byoc=<id>.<secret>`. Share the whole
  link. `room.rotateSecret()` revokes old links.
- The host can see and change everything; don't use BYOC for games where the
  host must not cheat.
