@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 drafts 07 through 17 of MoQ Transport. Zero runtime dependencies. Works in Node.js, Bun, Deno, and browsers.
Installation
Section titled “Installation”npm install @moqtap/codecQuick start
Section titled “Quick start”import { createCodec } from '@moqtap/codec';
const codec = createCodec({ draft: '17' });
// 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-17 (latest)import { createDraft17Codec, encodeMessage, decodeMessage, createStreamDecoder,} from '@moqtap/codec/draft17';
// Draft-14import { createDraft14Codec, encodeMessage, decodeMessage, createStreamDecoder,} from '@moqtap/codec/draft14';
// Draft-07import { createDraft07Codec, encodeMessage, decodeMessage,} from '@moqtap/codec/draft07';
// All drafts 07–17 have subpath exports: @moqtap/codec/draft07 through @moqtap/codec/draft17Codec 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({ codec: { draft: '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 { createDraft14SessionState } from '@moqtap/codec/draft14/session';
const session = createDraft14SessionState('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 |