# node-peer reference

Provide Node-hosted relay games with data-channel WebRTC using a `node-datachannel` native binding. Import from `@controller-plus/sdk/node-peer`. The SDK does not install or import that optional binding for you.

## Exports

`nativeIceServers(list = []): object[]` converts W3C `{ urls: string | string[], username?, credential? }` entries to native records. Non-array lists become empty. It accepts STUN and UDP TURN, strips IPv6 brackets, removes duplicates and skips malformed URLs over 512 characters. Default port is 3478. TURN requires string credentials. TCP/TLS TURN is skipped; native UDP support cannot promise connectivity on TCP-only networks.

STUN result: `{ hostname, port }`. TURN result: `{ hostname, port, username, password, relayType: 'TurnUdp' }`. Ports must be 1–65535. A `stun:...?...transport=tcp` URL still becomes the ordinary STUN record; no TCP transport field is retained.

`nodePeerConnection(binding, { log = 'Warning' } = {}): RTCPeerConnectionClass` requires `binding.PeerConnection` to be a function or throws `TypeError`. Initializes the binding logger once per binding if available; falsy `log` disables logger setup. Pass the returned class as `createRoom({ RTCPeerConnectionImpl })`. Close all rooms/links before calling its static `cleanup(): void`, which invokes optional `binding.cleanup()` and ignores cleanup exceptions.

## Returned peer class

`new RTCPeerConnection(config = {})` uses `config.iceServers` and disables automatic native negotiation. This is a small adapter for the SDK's peer link, not the complete browser WebRTC API: no audio/video, no ICE restart, and `setConfiguration` does nothing.

| Signature | Behavior and return |
| --- | --- |
| `createDataChannel(label, init = {}): Channel` | String label; supports `negotiated`, integer `id`, `ordered` (default true), integer `maxRetransmits` or `maxPacketLifeTime` (retransmits wins), string `protocol`. |
| `createOffer(): Promise<{ type: 'offer', sdp: '' }>` | Placeholder description; native SDP is produced by setting it. |
| `createAnswer(): Promise<{ type: 'answer', sdp: '' }>` | Same for answer. |
| `setLocalDescription(description = {}): Promise<void>` | Type defaults to answer if remote is offer, otherwise offer. Waits up to five seconds for native description. Rejects when closed, on native failure or timeout. Read `localDescription` after awaiting. |
| `setRemoteDescription(description): Promise<void>` | Require type offer/answer and string SDP; reject invalid/closed input or native failure. Set `remoteDescription`. |
| `addIceCandidate(candidate): Promise<void>` | String candidate or `{ candidate, sdpMid? }`; empty/end marker or closed peer is ignored. Default mid `'0'`. Native errors reject. |
| `setConfiguration(): void` | No-op. A fresh link is needed to change ICE servers. |
| `getStats(): Promise<Map>` | Empty without a selected pair; otherwise transport `T`, pair `CP`, local `L`, remote `R`. Pair has byte counters and optional `currentRoundTripTime` in seconds. |
| `close(): void` | Idempotently close native peer; set connection/ICE/signaling states to closed. |

Properties: `localDescription`, `remoteDescription` start null; `connectionState`, `iceConnectionState`, `iceGatheringState` start `'new'`; `signalingState` starts `'stable'`. `native` exposes the supplied binding's peer. Native state strings are forwarded.

| Callback property | Payload |
| --- | --- |
| `onicecandidate` | `{ type: 'icecandidate', candidate: { candidate, sdpMid, sdpMLineIndex: 0 } }`; candidate null at gathering completion. |
| `onconnectionstatechange`, `oniceconnectionstatechange`, `onicegatheringstatechange`, `onsignalingstatechange` | `{ type: '<matching event name>' }`; read current state on peer. Signaling callback requires binding support. |
| `ondatachannel` | `{ type: 'datachannel', channel }` when supported by binding. |

## Returned channel

Properties: `label`, `id` (default null), `negotiated`, `ordered`, `binaryType = 'arraybuffer'`, `readyState` (connecting/open/closing/closed), `bufferedAmount` (number; zero if native query fails), `native`.

`send(data): void` accepts string, Buffer, ArrayBuffer or typed array. Throws if not open or binary data has another shape. `close(): void` is idempotent; native close errors are ignored. This adapter uses callback properties, not a full EventTarget interface.

| Callback | Payload |
| --- | --- |
| `onopen` | `{ type: 'open' }` |
| `onclose` | `{ type: 'close' }` |
| `onerror` | `{ type: 'error', error: Error }` |
| `onmessage` | `{ type: 'message', data }`; text stays text, binary becomes ArrayBuffer by default. |

The adapter adds no media channel, payload-size limit or game protocol. `Session`/`PeerLink` apply protocol budgets. Native binding exceptions can propagate from construction and channel operations.

## Filter ICE servers and connect a game

The exported function below accepts a binding already loaded by your application. Keep the WebSocket fallback available when a peer route cannot connect.

<!-- example: node-peer -->
```js
import { createRoom } from '@controller-plus/sdk/relay-host';
import { nativeIceServers, nodePeerConnection } from '@controller-plus/sdk/node-peer';
console.log(nativeIceServers([{ urls: ['stun:stun.example:3478'] }]));

export async function startNativeGame(binding, options = {}) {
  const Peer = nodePeerConnection(binding, { log: 'Warning' });
  let room;
  try {
    room = await createRoom({
      ...options, game: { id: 'native-peer-example', name: 'Native peer game' },
      RTCPeerConnectionImpl: Peer, localControllers: false,
    });
  } catch (error) { Peer.cleanup(); throw error; }
  return { room, stop() { room.close(); Peer.cleanup(); } };
}
```

Your application calls `startNativeGame(binding)` with its compatible native module. Packaging and validating that binding on each target OS is separate from JavaScript integration.
