@moqtap/codec
Encode, decode, and validate MoQT messages in JavaScript/TypeScript.
What it does
Section titled “What it does”@moqtap/codec is a complete MoQT protocol library with two layers:
- Codec — stateless encode/decode of all control messages, data streams, and VarInts
- Session FSM — protocol state machine that validates message ordering, tracks subscriptions and fetches, and emits side effects
Supports draft-07 and draft-14 of MoQ Transport. Zero runtime dependencies. Works in Node.js, Bun, and browsers.
Installation
Section titled “Installation”npm install @moqtap/codecQuick start
Section titled “Quick start”import { createCodec } from '@moqtap/codec';
const codec = createCodec({ draft: '14' });
// Encode a message to wire bytesconst bytes = codec.encodeMessage({ type: 'subscribe', requestId: 1n, trackNamespace: ['live'], trackName: 'video', subscriberPriority: 128, groupOrder: 'original', forward: 0n, filterType: 'latest_group', parameters: new Map(),});
// Decode wire bytes back to a typed messageconst result = codec.decodeMessage(bytes);if (result.ok) { console.log(result.value.type); // 'subscribe'}Draft-specific imports
Section titled “Draft-specific imports”Each draft has its own entry point with the full API:
// Draft-14 (recommended)import { createDraft14Codec, encodeMessage, decodeMessage, createStreamDecoder,} from '@moqtap/codec/draft14';
// Draft-07import { createDraft07Codec, encodeMessage, decodeMessage,} from '@moqtap/codec/draft7';Codec API
Section titled “Codec API”Control messages
Section titled “Control messages”const codec = createDraft14Codec();
// Single messageconst bytes = codec.encodeMessage(message);const result = codec.decodeMessage(bytes);
// Streaming decoder for incremental parsingconst decoder = codec.createStreamDecoder();// TransformStream<Uint8Array, Draft14Message>Data streams (draft-14)
Section titled “Data streams (draft-14)”// Subgroup streamcodec.encodeSubgroupStream(stream);codec.decodeSubgroupStream(bytes);codec.createSubgroupStreamDecoder();
// Datagramscodec.encodeDatagram(obj);codec.decodeDatagram(bytes);
// Fetch streamscodec.encodeFetchStream(stream);codec.decodeFetchStream(bytes);codec.createFetchStreamDecoder();VarInt
Section titled “VarInt”import { encodeVarInt, decodeVarInt } from '@moqtap/codec/draft14';
const bytes = encodeVarInt(16384n); // 4-byte encodingconst result = decodeVarInt(bytes); // { ok: true, value: 16384n }Session state machine
Section titled “Session state machine”The session FSM validates protocol message ordering and tracks the lifecycle of subscriptions, publishes, and fetches. It does not perform I/O — you feed it messages and it tells you whether they’re legal.
import { createSessionState } from '@moqtap/codec/session';
const session = createSessionState({ draft: 'moq-transport-14', role: 'client' });Core methods
Section titled “Core methods”| Method | Description |
|---|---|
send(message) | Validate and apply an outgoing message. Returns TransitionResult. |
receive(message) | Validate and apply an incoming message. Returns TransitionResult. |
validateOutgoing(message) | Check if a message is legal to send without changing state. |
reset() | Return to idle phase, clear all tracked state. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
phase | SessionPhase | Current session phase |
role | 'client' or 'server' | The local endpoint’s role |
subscriptions | ReadonlyMap | Subscription states keyed by ID |
publishes | ReadonlyMap | Publish states keyed by request ID (draft-14) |
fetches | ReadonlyMap | Fetch states keyed by request ID (draft-14) |
legalOutgoing | ReadonlySet | Message types valid to send now |
legalIncoming | ReadonlySet | Message types valid to receive now |
TransitionResult
Section titled “TransitionResult”Every send() and receive() call returns a result:
// Success{ ok: true, phase: 'ready', sideEffects: [...] }
// Protocol violation{ ok: false, violation: { code: 'ROLE_VIOLATION', message: '...', currentPhase: 'ready', offendingMessage: 'subscribe_ok' } }Violation codes: MESSAGE_BEFORE_SETUP, UNEXPECTED_MESSAGE, DUPLICATE_SUBSCRIBE_ID, DUPLICATE_REQUEST_ID, UNKNOWN_SUBSCRIBE_ID, UNKNOWN_REQUEST_ID, ROLE_VIOLATION, STATE_VIOLATION, SETUP_VIOLATION.
Session phases
Section titled “Session phases”idle → setup → ready → draining| Phase | Entered when | Legal messages |
|---|---|---|
idle | Initial state | client_setup (client) |
setup | client_setup sent/received | server_setup (server) |
ready | server_setup received | All control messages |
draining | goaway sent/received | Existing subscriptions only |
Side effects
Section titled “Side effects”The FSM emits side effects to notify your application of state changes:
const result = session.receive(message);if (result.ok) { for (const effect of result.sideEffects) { switch (effect.type) { case 'session-ready': // Setup complete, can send control messages break; case 'session-draining': // GoAway received, effect.goAwayUri has the new URI break; case 'subscription-activated': // SUBSCRIBE_OK received, effect.subscribeId break; case 'subscription-ended': // Subscription closed, effect.reason break; case 'fetch-activated': // draft-14 case 'fetch-ended': // draft-14 case 'publish-activated': // draft-14 case 'publish-ended': // draft-14 break; } }}Example: full session lifecycle
Section titled “Example: full session lifecycle”import { createSessionState } from '@moqtap/codec/draft14/session';
const session = createSessionState({ role: 'client' });
// Setupsession.send({ type: 'client_setup', versions: [0xff00000en], parameters: new Map() });session.receive({ type: 'server_setup', selectedVersion: 0xff00000en, parameters: new Map() });// session.phase === 'ready'
// Subscribeconst sub = session.send({ type: 'subscribe', requestId: 1n, trackNamespace: ['chat'], trackName: 'messages', subscriberPriority: 128, groupOrder: 'original', forward: 0n, filterType: 'latest_object', parameters: new Map(),});// sub.ok === true, subscription is now 'pending'
session.receive({ type: 'subscribe_ok', requestId: 1n, expires: 0n, groupOrder: 'original', contentExists: true, parameters: new Map(),});// subscription is now 'active', sideEffects includes 'subscription-activated'
// Check what we can sendsession.legalOutgoing; // Set { 'subscribe', 'unsubscribe', 'fetch', ... }Decode errors
Section titled “Decode errors”Failed decodes throw or return error results with codes:
| Code | Meaning |
|---|---|
UNEXPECTED_END | Truncated input |
INVALID_VARINT | Malformed variable-length integer |
UNKNOWN_MESSAGE_TYPE | Unrecognized message type ID |
INVALID_PARAMETER | Malformed parameter encoding |
CONSTRAINT_VIOLATION | Value outside legal range |