# connection reference

Choose between developer-permitted routes. Import from `@controller-plus/sdk/connection`, the package root, or `/sdk/src/connection.mjs`. These lower-level adapters open a transport. They do not create rooms, authenticate a Session, or install new codecs into existing phone apps.

## Manifest validation

`validateConnectionManifest(value): ConnectionManifest` returns a copied manifest with routes sorted by ascending priority. Requires `v: 1` and 1–12 routes. Invalid input throws `Error` or a URL parsing error.

| Route field | Requirement/default |
| --- | --- |
| `id` | Unique string. |
| `transport` | `websocket`, `webrtc`, `native-udp`, `native-tcp`, or an extension matching `x-[a-z0-9][a-z0-9.-]{1,30}`. Recognition does not install an adapter. |
| `codec` | String naming a registered codec. |
| `scope` | `developer`, `local`, `peer` or `relay`. Relay requires `policy.allowOurRelay === true`. |
| `endpoint` | If truthy, valid ws/wss/http/https/udp/tcp URL without username/password. Transport-specific usability is checked when connecting. |
| `priority` | Finite number; defaults to input-array index. Lower first. |
| `timeout` | Defaults to 5000 ms; clamps to 500…15000. Supply a finite number. |
| `iceServers`, `iceTransportPolicy` | Passed to the WebRTC adapter; it defaults to `[]` and `'all'`. |

Other fields survive. The returned policy defaults `allowOurRelay` to false and retains supplied policy fields. This validator does not validate game state, permissions or every numeric field.

## TransportRegistry

`new TransportRegistry()` starts with no transports and a `json-v1` codec using `JSON.stringify` / `JSON.parse`. Public maps are `adapters` and `codecs`.

| Full signature | Return and behavior |
| --- | --- |
| `register(name, adapter): this` | Require `adapter.connect` function; replace an existing name. Throws otherwise. |
| `registerCodec(name, codec): this` | Require `encode`/`decode` functions; replace an existing name. Throws otherwise. |
| `connect(manifest, options = {}): Promise<{ connection, route, codec, attempts }>` | Validate, then try routes in order. `options` forwards to adapters with per-route `signal` and codec. `attempts` lists earlier failures as `{ route: id, reason }`. |

Unknown transport/codec adds `unsupported_adapter`. An HTTPS page trying an insecure ws/http endpoint adds `mixed_content`. A route timeout aborts the adapter and adds `Route timed out.`. An already-aborted outer signal throws `Connection cancelled.`; if none succeeds, the error message is `No permitted connection route succeeded.` with `.attempts`. Adapters must honor cancellation and clean up failed connections. A working route is kept; the registry does not later switch routes automatically.

## Built-in transports

`websocketTransport.connect(route, { signal, WebSocketImpl = globalThis.WebSocket } = {}): Promise<WebSocket>` resolves on open, rejects on error/cancellation. The caller must send hello and close the socket. No protocol wrapper or reconnect is added. A socket closing before open without an error can wait until the registry timeout.

`webRTCTransport.connect(route, { signal, signalOffer, RTCPeerConnectionImpl = globalThis.RTCPeerConnection } = {}): Promise<{ peer, channel, send, close }>` requires a peer constructor and `signalOffer(route, localDescription, { signal }): Promise<answer>`. It creates the `tap-input` data channel, unordered with zero retransmits, gathers candidates, obtains/applies the answer, then waits for the channel to open. `send(data)` calls channel.send; `close()` closes channel/peer. Errors close both before rejecting. Use registry timeout for signaling that never completes. This generic adapter is distinct from the two-channel peer protocol used by `Session`/`RelayRoom`.

The module has no SDK events. Use the returned socket/channel's normal events. Native TCP/UDP and `x-...` routes require your own compiled client adapter; a manifest cannot teach an unchanged browser a new transport.

## A permitted direct route

This example starts a local WebSocket echo endpoint, tries an unavailable extension first, then uses WebSocket. It shows transport selection only; use `ControllerHost` for an authenticated game endpoint.

<!-- example: connection -->
```js
import { WebSocketServer } from 'ws';
import { TransportRegistry, websocketTransport, validateConnectionManifest } from '@controller-plus/sdk/connection';
const wss = new WebSocketServer({ host: '127.0.0.1', port: 47412 });
await new Promise(resolve => wss.once('listening', resolve));
wss.on('connection', socket => socket.on('message', bytes => socket.send(bytes.toString())));
const registry = new TransportRegistry().register('websocket', websocketTransport);
registry.registerCodec('json-v1', { encode: JSON.stringify, decode: JSON.parse });
const manifest = validateConnectionManifest({ v: 1, policy: { allowOurRelay: false }, routes: [
  { id: 'extension', transport: 'x-example', scope: 'local', codec: 'json-v1' },
  { id: 'direct', transport: 'websocket', scope: 'developer', codec: 'json-v1', endpoint: 'ws://127.0.0.1:47412' },
] });
try {
  const { connection, codec, route, attempts } = await registry.connect(manifest);
  const reply = new Promise(resolve => connection.addEventListener('message', e => resolve(codec.decode(e.data)), { once: true }));
  connection.send(codec.encode({ hello: 'game' }));
  console.log(await reply, route.id, attempts);
  connection.close();
} finally { for (const socket of wss.clients) socket.terminate(); await new Promise(r => wss.close(r)); }
```

## WebRTC with game-owned signaling

This complete browser page connects two local peers. `signalOffer` stands in for your game's authenticated signaling service: it supplies an answer after ICE gathering. No STUN/TURN service is used in this local example. In a real game, exchange the offer/answer with the authorized remote game and provide your permitted ICE servers.

<!-- example: webrtc-transport -->
```html
<!doctype html>
<meta charset="utf-8"><title>WebRTC transport</title><p id="result">Connecting…</p>
<script type="module">
import { TransportRegistry, webRTCTransport } from 'https://controller.plus/sdk/src/connection.mjs';
const output = document.querySelector('#result');
const answerer = new RTCPeerConnection({ iceServers: [] });
let connection;
const gathered = peer => peer.iceGatheringState === 'complete' ? Promise.resolve() : new Promise(resolve => {
  const check = () => { if (peer.iceGatheringState === 'complete') { peer.removeEventListener('icegatheringstatechange', check); resolve(); } };
  peer.addEventListener('icegatheringstatechange', check);
});
answerer.ondatachannel = ({ channel }) => { channel.onmessage = e => channel.send(e.data); };
try {
  const registry = new TransportRegistry().register('webrtc', webRTCTransport);
  const result = await registry.connect({ v: 1, policy: { allowOurRelay: false }, routes: [
    { id: 'peer', transport: 'webrtc', scope: 'peer', codec: 'json-v1', timeout: 15000 },
  ] }, { async signalOffer(route, offer) {
    await answerer.setRemoteDescription(offer);
    await answerer.setLocalDescription(await answerer.createAnswer());
    await gathered(answerer); return answerer.localDescription;
  } });
  connection = result.connection;
  const reply = new Promise(resolve => connection.channel.addEventListener('message', e => resolve(result.codec.decode(e.data)), { once: true }));
  connection.send(result.codec.encode({ ready: true }));
  output.textContent = (await reply).ready ? 'Connected' : 'Unexpected reply';
} catch (error) { output.textContent = error.message; }
finally { connection?.close(); answerer.close(); }
</script>
```
