# wire reference

Encode compact input for the hot path. Browser/source import: `/sdk/src/wire.mjs`. There is no package `wire` export; applications using `Session` or `createRoom` do not need to import this module. Negotiate `wire: 1` in hello/welcome before sending binary input.

## Constants and frame layout

`WIRE_VERSION = 1`; `KIND` is frozen `{ INPUT: 1, PING: 2, PONG: 3, RELAYED: 4 }`. All multibyte numbers are little-endian. JSON envelopes remain version 1 and begin with `{`.

| Frame | Layout |
| --- | --- |
| Input | Byte 0: 1; bytes 1–2: uint16 sequence; byte 3: flags; bytes 4–5: button bitmask. Optional fields follow in flag order. |
| Flags | 1: four int8 sticks (×127); 2: two uint8 triggers at buttons 6/7 (×255); 4: three int16 gyro (×327); 8: three int16 accel (×327); 16: four int16 orientation (×32767). Other bits rejected. |
| Ping | Kind 2, uint16 ID, uint16 RTT; 65535 means unknown. Five bytes. |
| Pong | Kind 3, uint16 ID. Three bytes. |
| Relayed | Kind 4, uint8 ID byte length, UTF-8 player ID, inner frame. |

Input is 6–32 bytes: six for buttons only, ten with sticks, twelve with sticks and analog triggers. Non-trigger buttons use a ≥0.5 threshold. Omitted fields decode to zero/absent motion. Every frame is a full state. Sticks/triggers are quantized; do not compare received floats to arbitrary original decimals for exact equality.

## Codec functions

Binary arguments accept `ArrayBuffer` and typed array views. Encoders return `Uint8Array`.

| Full signature | Return and errors |
| --- | --- |
| `new WireError(message)` | Error with `name: 'WireError'`, `code: 'invalid_binary'`. |
| `isBinaryFrame(data): boolean` | False for strings; otherwise true if nonempty and first byte is not `{` (0x7b). This is classification, not validation. |
| `frameKind(data): number` | First byte or zero for empty bytes. |
| `encodeInput(input, seq): Uint8Array` | Missing axes/buttons become neutral; finite values quantized/clamped. Sequence wraps to uint16. Supply correctly sized motion vectors. |
| `decodeInput(data): { seq, input }` | Require kind 1, known flags and exact length; otherwise throw WireError. Does not range-check reserved int8 −128/int16 extremes from hand-crafted frames; use trusted encoder output or validate decoded ranges in your adapter. |
| `encodePing(id, roundTripMs = null)` | Wrap ID to uint16; round/clamp finite RTT to 0…65534; nonfinite/null becomes unknown. |
| `decodePing(data): { id, roundTripMs }` | Require five bytes and kind 2; unknown becomes null. |
| `encodePong(id)` | Wrap ID and encode three bytes. |
| `decodePong(data): { id }` | Require three bytes and kind 3. |
| `encodeRelayed(playerId, frame)` | ID must encode to 1–64 UTF-8 bytes, otherwise WireError. Inner bytes are not validated. |
| `decodeRelayed(data): { playerId, frame }` | Require nonempty ID and inner frame. Returns inner subarray without validating its type. Encoder enforces 64-byte ID cap; decoder does not. |
| `seqNewer(a, b): boolean` | True if uint16 distance from b to a is 1…32767. Handles wrap; equal and half-range distance are not newer. |

Malformed ping/pong/relayed frames throw WireError. Invalid JavaScript argument shapes can throw normal errors. Codec calls emit no events.

## InputPacer

`new InputPacer({ send, now = () => performance.now(), setTimer = (fn, ms) => setTimeout(fn, ms), clearTimer = timer => clearTimeout(timer) } = {})`

Supply `send(bytes, { repeat, edge })`. `repeat` marks a copy/held refresh; `edge` marks a button-bitmask change. The callback's return value is ignored and exceptions propagate. Times are milliseconds. New state starts neutral with sequence zero.

| Method | Behavior |
| --- | --- |
| `configure({ minIntervalMs = 16, repeatsMs = [], refreshMs = 400, settleMs = 100 } = {}): void` | Replace analog pacing, repeat delays, held refresh and one-quantization-step jitter delay. Values are not validated. |
| `update(input): boolean` | Copy full state and optional motion; true when sent immediately, false when unchanged or scheduled. Button edges send immediately; analog changes may coalesce. |
| `flush(): void` | Send current state now with a new sequence. |
| `reset(): void` | Stop timers and forget sent-state comparison; does not reset sequence or current state. |
| `stop(): void` | Cancel all pacing timers. Does not send neutral input. |

`RELEASE_COPIES_MS` is frozen `[30, 100, 250]`. Becoming neutral schedules these copies on every route instead of ordinary repeat delays; newer input cancels their effect. Held nonneutral input refreshes at `refreshMs`; neutral input is quiet after release copies. Call `stop` during teardown. `Session` selects [route-specific policies](sdk-client.md#pacing-and-limits).

## InputTrack

`new InputTrack({ staleMs = 1200, now = () => performance.now() } = {})` holds public fields `seq` (initial null), `input` (neutral), `at` (initial −Infinity).

| Method | Return and behavior |
| --- | --- |
| `apply(seq, input): boolean` | Newer sequence replaces state and returns true. Same sequence refreshes liveness but returns false. Old sequence is ignored without refreshing. Stores the input object directly. |
| `set(input): void` | Replace input and refresh time without changing sequence. |
| `restart(): void` | Clear sequence only, for a resumed phone whose counter restarted. |
| `stale(): boolean` | True if nonneutral input has not refreshed for more than `staleMs`. Does not clear it; host must call `set(NEUTRAL())` and update the game. |

## Encode, pace and receive

<!-- example: wire -->
```js
import { NEUTRAL } from 'https://controller.plus/sdk/src/controls.mjs';
import { encodeInput, decodeInput, encodeRelayed, decodeRelayed, encodePing, decodePing,
  encodePong, decodePong, seqNewer, isBinaryFrame, frameKind, InputPacer, InputTrack
} from 'https://controller.plus/sdk/src/wire.mjs';
const input = NEUTRAL(); input.buttons[0] = 1;
const bytes = encodeInput(input, 65535);
const wrapped = decodeRelayed(encodeRelayed('p1', bytes));
console.log(wrapped.playerId, decodeInput(wrapped.frame));
console.log(decodePing(encodePing(7, 12)), decodePong(encodePong(7)));
console.log(seqNewer(0, 65535), isBinaryFrame(bytes), frameKind(bytes));
let time = 0;
const track = new InputTrack({ now: () => time });
const pacer = new InputPacer({ send(frame) {
  const { seq, input } = decodeInput(frame); track.apply(seq, input);
} });
pacer.configure({ minIntervalMs: 16, refreshMs: 400 });
pacer.update(input); pacer.flush();
time = 1300;
if (track.stale()) track.set(NEUTRAL());
track.restart(); pacer.reset(); pacer.stop();
console.log(track.input.buttons[0]); // 0
```
