Skip to content
Merged
1 change: 1 addition & 0 deletions AGENTS.md
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This file provides guidance for AI coding agents when working with code in this repository.

## Project Overview

Expand All @@ -14,7 +14,7 @@ just fix # Auto-fix linting issues
just build # Build all packages
```

Except when being run in the Claude Code on the web: Use cargo/bun directly.
If `just` is unavailable, use `cargo` or `bun` directly.

## Architecture

Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ web-async = { version = "0.1.1", features = ["tracing"] }
web-transport-iroh = "0.1.1"
web-transport-quiche = "0.2"
web-transport-quinn = "0.11"
web-transport-trait = "0.3"
web-transport-ws = "0.2"
web-transport-trait = "0.3.2"
web-transport-ws = "0.2.2"

[profile.dev]
panic = "abort"
Expand Down
45 changes: 34 additions & 11 deletions js/lite/src/connection/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,34 @@ export async function connect(url: URL, props?: ConnectProps): Promise<Establish
}

// Race them, using `.any` to ignore if one participant has a error.
const quic = await Promise.any(
const session = await Promise.any(
webtransport ? (websocket ? [websocket, webtransport] : [webtransport]) : [websocket],
);
if (done) done();

if (!quic) throw new Error("no transport available");
if (!session) throw new Error("no transport available");

// Save if WebSocket won the last race, so we won't give QUIC a head start next time.
if (quic instanceof WebTransportWs) {
if (session instanceof WebTransportWs) {
console.warn(url.toString(), "using WebSocket fallback; the user experience may be degraded");
websocketWon.add(url.toString());
}

// moq-rs currently requires the ROLE extension to be set.
const stream = await Stream.open(quic);
const stream = await Stream.open(session);

// @ts-expect-error - TODO: add protocol to WebTransport
const protocol = session instanceof WebTransport ? session.protocol : undefined;

// Choose setup encoding based on negotiated WebTransport protocol (if any).
let setupVersion: Ietf.Version;
if (protocol === Ietf.ALPN.DRAFT_15) {
setupVersion = Ietf.Version.DRAFT_15;
} else if (protocol === undefined) {
setupVersion = Ietf.Version.DRAFT_14;
} else {
throw new Error(`unsupported WebTransport protocol: ${protocol}`);
}
Comment on lines +76 to +87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

WebTransport.protocol return value empty string vs undefined ALPN negotiation

💡 Result:

WebTransport.protocol is specified as a readonly DOMString, so in spec-compliant implementations it should not be undefined; it should be a string (possibly ""). (w3.org)

When you should see "" (empty string)

Per the WebTransport spec, protocol returns the server-selected application protocol only if:

  1. the session is established and
  2. you passed a non-empty protocols array in the constructor options;
    otherwise it returns the empty string. (w3.org)

Concretely, "" can mean:

  • you didn’t provide protocols, or provided []; or
  • you provided protocols, but the server didn’t select one (i.e., no effective WT-Protocol choice was made / honored).

How this relates to “ALPN”

WebTransport’s “ALPN-like” negotiation is not TLS ALPN; it’s done at the WebTransport/HTTP3 layer using:

  • client: WT-Available-Protocols
  • server: WT-Protocol (must be one of the client’s offered values, otherwise it’s ignored) (datatracker.ietf.org)

So WebTransport.protocol corresponds to the result of the WT-Protocol selection (if any), not the TLS ALPN value. (datatracker.ietf.org)

If you’re seeing undefined

That’s not what the spec describes for protocol; it usually indicates an environment/implementation mismatch (older/experimental browser build, polyfill, or a non-browser runtime API surface), rather than a meaningful “no protocol negotiated” signal. (w3.org)

Citations:


session.protocol returns an empty string (not undefined) when no ALPN protocol is negotiated per the WebTransport spec.

Per the W3C WebTransport specification, protocol is a readonly DOMString that returns "" (empty string) when no protocol is negotiated, not undefined. The current code on line 83 checks protocol === undefined, which would not catch the empty string case. If no protocol is selected, the condition would fail and execution would fall through to the else branch on line 85, throwing an "unsupported WebTransport protocol" error.

🐛 Proposed fix: handle empty string the same as undefined
 	if (protocol === Ietf.ALPN.DRAFT_15) {
 		setupVersion = Ietf.Version.DRAFT_15;
-	} else if (protocol === undefined) {
+	} else if (!protocol) {
 		setupVersion = Ietf.Version.DRAFT_14;
 	} else {
 		throw new Error(`unsupported WebTransport protocol: ${protocol}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// @ts-expect-error - TODO: add protocol to WebTransport
const protocol = session instanceof WebTransport ? session.protocol : undefined;
// Choose setup encoding based on negotiated WebTransport protocol (if any).
let setupVersion: Ietf.Version;
if (protocol === Ietf.ALPN.DRAFT_15) {
setupVersion = Ietf.Version.DRAFT_15;
} else if (protocol === undefined) {
setupVersion = Ietf.Version.DRAFT_14;
} else {
throw new Error(`unsupported WebTransport protocol: ${protocol}`);
}
// `@ts-expect-error` - TODO: add protocol to WebTransport
const protocol = session instanceof WebTransport ? session.protocol : undefined;
// Choose setup encoding based on negotiated WebTransport protocol (if any).
let setupVersion: Ietf.Version;
if (protocol === Ietf.ALPN.DRAFT_15) {
setupVersion = Ietf.Version.DRAFT_15;
} else if (!protocol) {
setupVersion = Ietf.Version.DRAFT_14;
} else {
throw new Error(`unsupported WebTransport protocol: ${protocol}`);
}
🤖 Prompt for AI Agents
In `@js/lite/src/connection/connect.ts` around lines 76 - 87, The code currently
treats session.protocol === undefined as "no ALPN", but per the WebTransport
spec session.protocol can be an empty string when no protocol is negotiated;
update the logic around the protocol variable (session.protocol) and the
setupVersion selection so that an empty string ('') is treated the same as
undefined: if protocol equals Ietf.ALPN.DRAFT_15 set setupVersion =
Ietf.Version.DRAFT_15, otherwise if protocol is falsy or '' set setupVersion =
Ietf.Version.DRAFT_14, else throw the same unsupported WebTransport protocol
error; adjust the condition checks near the protocol declaration and the
setupVersion assignment to reference session.protocol / protocol accordingly.


// We're encoding 0x20 so it's backwards compatible with moq-transport-10+
await stream.writer.u53(Lite.StreamId.ClientCompat);
Expand All @@ -82,26 +95,34 @@ export async function connect(url: URL, props?: ConnectProps): Promise<Establish
params.setVarint(Ietf.Parameter.MaxRequestId, 42069n); // Allow a ton of request IDs.
params.setBytes(Ietf.Parameter.Implementation, encoder.encode("moq-lite-js")); // Put the implementation name in the parameters.

const client = new Ietf.ClientSetup([Lite.Version.DRAFT_02, Lite.Version.DRAFT_01, Ietf.Version.DRAFT_14], params);
const client = new Ietf.ClientSetup(
// NOTE: draft 15 onwards does not use CLIENT_SETUP to negotiate the version.
// We still echo it just to make sure we're not accidentally trying to negotiate the version.
setupVersion === Ietf.Version.DRAFT_15
? [Ietf.Version.DRAFT_15]
: [Lite.Version.DRAFT_02, Lite.Version.DRAFT_01, Ietf.Version.DRAFT_14],
params,
);
console.debug(url.toString(), "sending client setup", client);
await client.encode(stream.writer);
await client.encode(stream.writer, setupVersion);

// And we expect 0x21 as the response.
const serverCompat = await stream.reader.u53();
if (serverCompat !== Lite.StreamId.ServerCompat) {
throw new Error(`unsupported server message type: ${serverCompat.toString()}`);
}

const server = await Ietf.ServerSetup.decode(stream.reader);
// Decode ServerSetup in Draft14 format (version + params)
const server = await Ietf.ServerSetup.decode(stream.reader, setupVersion);
console.debug(url.toString(), "received server setup", server);

if (Object.values(Lite.Version).includes(server.version as Lite.Version)) {
console.debug(url.toString(), "moq-lite session established");
return new Lite.Connection(url, quic, stream, server.version as Lite.Version);
return new Lite.Connection(url, session, stream, server.version as Lite.Version);
} else if (Object.values(Ietf.Version).includes(server.version as Ietf.Version)) {
const maxRequestId = server.parameters.getVarint(Ietf.Parameter.MaxRequestId) ?? 0n;
console.debug(url.toString(), "moq-ietf session established");
return new Ietf.Connection(url, quic, stream, maxRequestId);
console.debug(url.toString(), "moq-ietf session established, version:", server.version.toString(16));
return new Ietf.Connection(url, session, stream, maxRequestId, server.version as Ietf.IetfVersion);
} else {
throw new Error(`unsupported server version: ${server.version.toString()}`);
}
Expand All @@ -117,6 +138,8 @@ async function connectWebTransport(
const finalOptions: WebTransportOptions = {
allowPooling: false,
congestionControl: "low-latency",
// @ts-expect-error - TODO: add protocols to WebTransportOptions
protocols: [Ietf.ALPN.DRAFT_15],
...options,
};

Expand Down Expand Up @@ -161,7 +184,7 @@ async function connectWebTransport(
}

// TODO accept arguments to control the port/path used.
async function connectWebSocket(url: URL, delay: number, cancel: Promise<void>): Promise<WebTransport | undefined> {
async function connectWebSocket(url: URL, delay: number, cancel: Promise<void>): Promise<WebTransportWs | undefined> {
const timer = new Promise<void>((resolve) => setTimeout(resolve, delay));

const active = await Promise.race([cancel, timer.then(() => true)]);
Expand Down
17 changes: 14 additions & 3 deletions js/lite/src/ietf/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
PublishNamespaceOk,
} from "./publish_namespace.ts";
import { Publisher } from "./publisher.ts";
import { MaxRequestId, RequestsBlocked } from "./request.ts";
import { MaxRequestId, RequestError, RequestOk, RequestsBlocked } from "./request.ts";
import * as Setup from "./setup.ts";
import { Subscribe, SubscribeError, SubscribeOk, Unsubscribe } from "./subscribe.ts";
import {
Expand All @@ -28,6 +28,7 @@ import {
} from "./subscribe_namespace.ts";
import { Subscriber } from "./subscriber.ts";
import { TrackStatus, TrackStatusRequest } from "./track.ts";
import type { IetfVersion } from "./version.ts";

/**
* Represents a connection to a MoQ server using moq-transport protocol.
Expand Down Expand Up @@ -58,13 +59,15 @@ export class Connection implements Established {
* @param url - The URL of the connection
* @param quic - The WebTransport session
* @param controlStream - The control stream
* @param maxRequestId - The initial max request ID
* @param version - The negotiated protocol version
*
* @internal
*/
constructor(url: URL, quic: WebTransport, control: Stream, maxRequestId: bigint) {
constructor(url: URL, quic: WebTransport, control: Stream, maxRequestId: bigint, version?: IetfVersion) {
this.url = url;
this.#quic = quic;
this.#control = new Control.Stream(control, maxRequestId);
this.#control = new Control.Stream(control, maxRequestId, version);

this.#quic.closed.finally(() => {
this.#control.close();
Expand Down Expand Up @@ -203,6 +206,14 @@ export class Connection implements Established {
this.#control.maxRequestId(msg.requestId);
} else if (msg instanceof RequestsBlocked) {
console.warn("ignoring REQUESTS_BLOCKED message");
} else if (msg instanceof RequestOk) {
// v15: Route RequestOk to both publisher and subscriber
await this.#publisher.handleRequestOk(msg);
await this.#subscriber.handleRequestOk(msg);
} else if (msg instanceof RequestError) {
// v15: Route RequestError to both publisher and subscriber
await this.#publisher.handleRequestError(msg);
await this.#subscriber.handleRequestError(msg);
} else {
unreachable(msg);
}
Expand Down
61 changes: 49 additions & 12 deletions js/lite/src/ietf/control.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Mutex } from "async-mutex";
import type { Stream as StreamInner } from "../stream.ts";
import type { Reader, Stream as StreamInner, Writer } from "../stream.ts";
import { Fetch, FetchCancel, FetchError, FetchOk } from "./fetch.ts";
import { GoAway } from "./goaway.ts";
import { Publish, PublishDone, PublishError, PublishOk } from "./publish.ts";
Expand All @@ -10,7 +10,7 @@ import {
PublishNamespaceError,
PublishNamespaceOk,
} from "./publish_namespace.ts";
import { MaxRequestId, RequestsBlocked } from "./request.ts";
import { MaxRequestId, RequestError, RequestOk, RequestsBlocked } from "./request.ts";
import * as Setup from "./setup.ts";
import { Subscribe, SubscribeError, SubscribeOk, Unsubscribe } from "./subscribe.ts";
import {
Expand All @@ -20,11 +20,10 @@ import {
UnsubscribeNamespace,
} from "./subscribe_namespace.ts";
import { TrackStatus, TrackStatusRequest } from "./track.ts";
import { type IetfVersion, Version } from "./version.ts";

/**
* Control message types as defined in moq-transport-14
*/
const Messages = {
// v14 message map — IDs that have different meanings in v15 are handled specially
const MessagesV14 = {
[Setup.ClientSetup.id]: Setup.ClientSetup,
[Setup.ServerSetup.id]: Setup.ServerSetup,
[Subscribe.id]: Subscribe,
Expand Down Expand Up @@ -55,15 +54,42 @@ const Messages = {
[RequestsBlocked.id]: RequestsBlocked,
} as const;

export type MessageId = keyof typeof Messages;
// v15 message map — 0x05 → RequestError, 0x07 → RequestOk (different wire format)
// Messages removed in v15 (0x08, 0x0E, 0x12, 0x13, 0x19, 0x1E, 0x1F) are excluded and will be rejected
const MessagesV15 = {
[Setup.ClientSetup.id]: Setup.ClientSetup,
[Setup.ServerSetup.id]: Setup.ServerSetup,
[Subscribe.id]: Subscribe,
[SubscribeOk.id]: SubscribeOk,
[RequestError.id]: RequestError, // 0x05 → RequestError instead of SubscribeError
[PublishNamespace.id]: PublishNamespace,
[RequestOk.id]: RequestOk, // 0x07 → RequestOk instead of PublishNamespaceOk
[PublishNamespaceDone.id]: PublishNamespaceDone,
[Unsubscribe.id]: Unsubscribe,
[PublishDone.id]: PublishDone,
[PublishNamespaceCancel.id]: PublishNamespaceCancel,
[TrackStatusRequest.id]: TrackStatusRequest,
[GoAway.id]: GoAway,
[Fetch.id]: Fetch,
[FetchCancel.id]: FetchCancel,
[FetchOk.id]: FetchOk,
[SubscribeNamespace.id]: SubscribeNamespace,
[UnsubscribeNamespace.id]: UnsubscribeNamespace,
[Publish.id]: Publish,
[MaxRequestId.id]: MaxRequestId,
[RequestsBlocked.id]: RequestsBlocked,
} as const;
Comment on lines +57 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the control.ts file
find . -name "control.ts" -path "*/lite/src/ietf/*" -type f

Repository: moq-dev/moq

Length of output: 84


🏁 Script executed:

# Read the control.ts file to verify the MessagesV15 map
cat -n js/lite/src/ietf/control.ts | head -100

Repository: moq-dev/moq

Length of output: 4540


🏁 Script executed:

# Search for TrackStatus definition and usage
rg "TrackStatus" js/lite/src/ietf/control.ts -B2 -A2

Repository: moq-dev/moq

Length of output: 619


🌐 Web query:

moq-transport draft-15 TRACK_STATUS message specification

💡 Result:

In draft-ietf-moq-transport-15, TRACK_STATUS is a control-stream request a potential subscriber sends to learn the current status of a track. (datatracker.ietf.org)

Wire format / message type

Semantics

  • The receiver processes TRACK_STATUS as if it were SUBSCRIBE, except it:
    • does not create downstream subscription state, and
    • does not send any Objects. (datatracker.ietf.org)

Responses

  • Success: publisher replies with REQUEST_OK, with the same parameters it would have set in SUBSCRIBE_OK; Track Alias is not used. (datatracker.ietf.org)
  • Failure: publisher replies with REQUEST_ERROR. (datatracker.ietf.org)

Important behavioral constraints

  • Publisher does not send PUBLISH_DONE for TRACK_STATUS.
  • Subscriber cannot send SUBSCRIBE_UPDATE or UNSUBSCRIBE for a TRACK_STATUS request. (datatracker.ietf.org)

If you want, paste the SUBSCRIBE message definition you’re using (or your implementation struct), and I’ll map it 1:1 to TRACK_STATUS for draft-15.

Citations:


🏁 Script executed:

# Check the track.ts file for message IDs
cat js/lite/src/ietf/track.ts

Repository: moq-dev/moq

Length of output: 2590


🏁 Script executed:

# Also check if there's specification documentation or comments about the IDs
rg "0x0D|0x0C" js/lite/src/ietf/ -B2 -A2

Repository: moq-dev/moq

Length of output: 37


🌐 Web query:

moq-transport draft-15 TRACK_STATUS message type 0x0D removed or kept

💡 Result:

In draft-ietf-moq-transport-15, TRACK_STATUS is kept as a control message, and its Message Type is 0x0D. It’s sent by a (potential) subscriber and uses the same on-wire format as SUBSCRIBE; the publisher replies with REQUEST_OK (success) or REQUEST_ERROR (failure). [1]

Note: in older drafts (e.g., draft-ietf-moq-transport-05), 0x0D was TRACK_STATUS_REQUEST and TRACK_STATUS was 0x0E, so the type code changed in later drafts. [2]

Sources:
[1] draft-ietf-moq-transport-15 (Control Message Types table; Section 9.19 TRACK_STATUS) (datatracker.ietf.org)
[2] draft-ietf-moq-transport-05 (message type assignments showing 0x0D TRACK_STATUS_REQUEST, 0x0E TRACK_STATUS) (datatracker.ietf.org)

Citations:


TrackStatus is missing from MessagesV15 — cannot decode 0x0E messages on v15 path.

MessagesV15 (line 59–81) excludes TrackStatus (id 0x0E), while MessagesV14 includes it (line 40). If a v15 peer sends a TrackStatus message, the read() method will throw "Unknown control message type" because the message map lookup will fail.

The code comment acknowledges 0x0E is intentionally removed in v15, but this warrants clarity: draft-ietf-moq-transport-15 does specify a TRACK_STATUS message (0x0D in the spec). The implementation uses draft-05-style IDs (TrackStatusRequest=0x0d, TrackStatus=0x0e), which may indicate either a version mismatch or an intentional deviation. Confirm whether v15 support should include TrackStatus decoding.

Proposed fix (if TrackStatus should be supported in v15)
 const MessagesV15 = {
 	...
 	[TrackStatusRequest.id]: TrackStatusRequest,
+	[TrackStatus.id]: TrackStatus,
 	[GoAway.id]: GoAway,
 	...
 } as const;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// v15 message map — 0x05 → RequestError, 0x07 → RequestOk (different wire format)
// Messages removed in v15 (0x08, 0x0E, 0x12, 0x13, 0x19, 0x1E, 0x1F) are excluded and will be rejected
const MessagesV15 = {
[Setup.ClientSetup.id]: Setup.ClientSetup,
[Setup.ServerSetup.id]: Setup.ServerSetup,
[Subscribe.id]: Subscribe,
[SubscribeOk.id]: SubscribeOk,
[RequestError.id]: RequestError, // 0x05 → RequestError instead of SubscribeError
[PublishNamespace.id]: PublishNamespace,
[RequestOk.id]: RequestOk, // 0x07 → RequestOk instead of PublishNamespaceOk
[PublishNamespaceDone.id]: PublishNamespaceDone,
[Unsubscribe.id]: Unsubscribe,
[PublishDone.id]: PublishDone,
[PublishNamespaceCancel.id]: PublishNamespaceCancel,
[TrackStatusRequest.id]: TrackStatusRequest,
[GoAway.id]: GoAway,
[Fetch.id]: Fetch,
[FetchCancel.id]: FetchCancel,
[FetchOk.id]: FetchOk,
[SubscribeNamespace.id]: SubscribeNamespace,
[UnsubscribeNamespace.id]: UnsubscribeNamespace,
[Publish.id]: Publish,
[MaxRequestId.id]: MaxRequestId,
[RequestsBlocked.id]: RequestsBlocked,
} as const;
// v15 message map — 0x05 → RequestError, 0x07 → RequestOk (different wire format)
// Messages removed in v15 (0x08, 0x0E, 0x12, 0x13, 0x19, 0x1E, 0x1F) are excluded and will be rejected
const MessagesV15 = {
[Setup.ClientSetup.id]: Setup.ClientSetup,
[Setup.ServerSetup.id]: Setup.ServerSetup,
[Subscribe.id]: Subscribe,
[SubscribeOk.id]: SubscribeOk,
[RequestError.id]: RequestError, // 0x05 → RequestError instead of SubscribeError
[PublishNamespace.id]: PublishNamespace,
[RequestOk.id]: RequestOk, // 0x07 → RequestOk instead of PublishNamespaceOk
[PublishNamespaceDone.id]: PublishNamespaceDone,
[Unsubscribe.id]: Unsubscribe,
[PublishDone.id]: PublishDone,
[PublishNamespaceCancel.id]: PublishNamespaceCancel,
[TrackStatusRequest.id]: TrackStatusRequest,
[TrackStatus.id]: TrackStatus,
[GoAway.id]: GoAway,
[Fetch.id]: Fetch,
[FetchCancel.id]: FetchCancel,
[FetchOk.id]: FetchOk,
[SubscribeNamespace.id]: SubscribeNamespace,
[UnsubscribeNamespace.id]: UnsubscribeNamespace,
[Publish.id]: Publish,
[MaxRequestId.id]: MaxRequestId,
[RequestsBlocked.id]: RequestsBlocked,
} as const;
🤖 Prompt for AI Agents
In `@js/lite/src/ietf/control.ts` around lines 57 - 81, MessagesV15 currently
omits TrackStatus so read() cannot decode 0x0E messages; decide whether v15
should support TrackStatus and if so add TrackStatus to the MessagesV15 map
(mirroring MessagesV14 entry), ensuring the mapping uses the same identifier
(TrackStatus.id) and that TrackStatusRequest/TrackStatus id values align with
the v15 spec; update MessagesV15 to include TrackStatus and run tests of the
read() path to verify 0x0E/0x0D decoding is handled correctly.


export type MessageType = (typeof Messages)[keyof typeof Messages];
type V14MessageType = (typeof MessagesV14)[keyof typeof MessagesV14];
type V15MessageType = (typeof MessagesV15)[keyof typeof MessagesV15];
type MessageType = V14MessageType | V15MessageType;

// Type for control message instances (not constructors)
export type Message = InstanceType<MessageType>;

export class Stream {
stream: StreamInner;
version: IetfVersion;

// The client always starts at 0.
#requestId = 0n;
Expand All @@ -76,8 +102,9 @@ export class Stream {
#writeLock = new Mutex();
#readLock = new Mutex();

constructor(stream: StreamInner, maxRequestId: bigint) {
constructor(stream: StreamInner, maxRequestId: bigint, version: IetfVersion = Version.DRAFT_14) {
this.stream = stream;
this.version = version;
this.#maxRequestId = maxRequestId;
this.#maxRequestIdPromise = new Promise((resolve) => {
this.#maxRequestIdResolve = resolve;
Expand All @@ -96,7 +123,8 @@ export class Stream {
await this.stream.writer.u53((message.constructor as MessageType).id);

// Write message payload with u16 size prefix
await message.encode(this.stream.writer);
// Extra version arg is silently ignored by messages that don't need it
await (message.encode as (w: Writer, v?: IetfVersion) => Promise<void>)(this.stream.writer, this.version);
});
}

Expand All @@ -107,12 +135,21 @@ export class Stream {
async read(): Promise<Message> {
return await this.#readLock.runExclusive(async () => {
const messageType = await this.stream.reader.u53();
if (!(messageType in Messages)) {

const messages = this.version === Version.DRAFT_15 ? MessagesV15 : MessagesV14;
if (!(messageType in messages)) {
throw new Error(`Unknown control message type: ${messageType}`);
}

try {
const msg = await Messages[messageType].decode(this.stream.reader);
const msgClass = messages[messageType as keyof typeof messages];

// Extra version arg is silently ignored by messages that don't need it
const msg = await (msgClass as { decode: (r: Reader, v?: IetfVersion) => Promise<Message> }).decode(
this.stream.reader,
this.version,
);

console.debug("message read", msg);
return msg;
} catch (err) {
Expand Down
Loading
Loading