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.
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.
<!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>