Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion js/net/src/lite/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class Group {
}
}

/** Decode an unsigned zigzag varint back to a signed delta (mirrors Rust `VarInt::to_zigzag`). */
/** Decode an unsigned zigzag varint back to a signed delta (mirrors Rust `varint::unzigzag`). */
function unzigzag(v: bigint): bigint {
return (v >> 1n) ^ -(v & 1n);
}
Expand Down
2 changes: 1 addition & 1 deletion js/net/src/lite/publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const PROBE_MAX_AGE = 10_000; // ms
const PROBE_MAX_DELTA = 0.25;
const PROBE_RTT_DELTA = 0.25;

/** Map a signed delta to an unsigned zigzag varint value (mirrors Rust `VarInt::from_zigzag`). */
/** Map a signed delta to an unsigned zigzag varint value (mirrors Rust `varint::zigzag`). */
function zigzag(delta: bigint): bigint {
return delta >= 0n ? delta << 1n : (-delta << 1n) - 1n;
}
Expand Down
16 changes: 8 additions & 8 deletions quest/m1/rs2ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ Decided in planning (2026-09-27), with the spike data in
and bytes out, no runtime. The async helper methods move behind an `async`
cargo feature; rs2ts reads the crate without it and JS reimplements the
helpers with Promises. No second crate.
- Varints are not bounded to 2^53 on the wire: 62 bits in QUIC form, 64 in
leading-ones form. JS holds any `u64` as a `U64` with checked conversion to
and from `number`; varints are only its wire encoding.
- Values are plain `u64` in Rust, and varint is a wire encoding in the codec,
not a type. The spec is not bounded to 2^53: the leading-ones form
(moq-transport draft-17+) carries all 64 bits, and the QUIC form (moq-lite,
drafts 14-16) refuses anything past 2^62 - 1 rather than truncating. Rust
`u64` maps to a TypeScript `U64` (two `u32` halves), generically, with
checked conversion to and from `number`.
- The generated TypeScript is committed and a CI lane regenerates it and
fails on drift, so JS contributors and npm publishing never need the
nightly toolchain Charon pins. It lives inside js/net and `@moq/net` stays
Expand All @@ -54,24 +57,21 @@ js/net it replaces, measured with the [browser benchmarks](/quest/m1/browser-ben

## Required

- [VarInt codec](/quest/m1/rs2ts/varint-codec.md) - moq-net encodes through a `VarInt` newtype and a concrete slice-based codec, not generic traits on primitives
- [rs2ts](/quest/m1/rs2ts/translator.md) - a Charon-based translator emits readable TypeScript for moq-net's lite codec, committed and checked for drift in CI
- [Sans-IO moq-net](/quest/m1/rs2ts/sans-io/README.md) - moq-net builds and runs without a runtime; async helpers sit behind an `async` feature
- [Mock-clock tests](/quest/m1/rs2ts/mock-clock.md) - moq-net's tests run on the sans-IO clock instead of tokio, so they translate with the code
- [Generated lite](/quest/m1/rs2ts/lite.md) - @moq/net's lite session and model layer are generated from moq-net
- [IETF parameters](/quest/m1/rs2ts/ietf-params.md) - the IETF codec drops its `Param` trait on primitives, so it translates like lite
- [Generated IETF](/quest/m1/rs2ts/ietf.md) - @moq/net's moq-transport session is generated too
- [Remove moq-wasm](/quest/m1/rs2ts/remove-wasm.md) - the WASM experiment is deleted once generated lite ships
- [Browser benchmarks](/quest/m1/browser-benchmarks.md) - the harness the no-downgrade report uses

## Closes

- [#2907](https://github.com/moq-dev/moq/issues/2907) - close this issue when the quest finishes
- [#2822](https://github.com/moq-dev/moq/issues/2822) - close this issue when the quest finishes
- [#2835](https://github.com/moq-dev/moq/issues/2835) - close this issue when the quest finishes

## Required

- [Browser benchmarks](/quest/m1/browser-benchmarks.md) - the harness the no-downgrade report uses

## Related

- [#2850](/quest/m1/2850-js-net-give-reader-a-synchronous-decode-so-the-publisher.md) - the same synchronous decode shape, in hand-written js/net today
25 changes: 25 additions & 0 deletions quest/m1/rs2ts/ietf-params.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# [S] IETF parameters without primitive traits

## Goal

moq-net's IETF message parameters encode and decode through concrete
per-kind methods, with no `Param` impls on `u8`, `bool`, `u64`, or
`Option<T>`, so the IETF codec has the same concrete shape as the lite codec
the translator targets.

## Plan

The VarInt codec (#4463) removed Encode/Decode on
primitives, but `ietf/parameters.rs` still implements its own `Param` trait on
them, which rs2ts can only translate with dictionary passing. Replace the
impls with methods on the concrete `Decoder`/`Encoder` (or on the parameter
kinds), keeping the draft-14..16 varint cast and the draft-17+ forms byte for
byte. Benchmark IETF message encode and decode before and after with
`--bench codec`.

Public API: none (`ietf` parameters are crate-private). Lands on `dev` with
the line. Wire: none.

## Required

- #4463 (VarInt codec) merged into the questline, since this builds on its `Decoder`/`Encoder`
1 change: 1 addition & 0 deletions quest/m1/rs2ts/ietf.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ Public API: breaks `@moq/net`; retargets to `dev`. Wire: none.

- [Generated lite](/quest/m1/rs2ts/lite.md) - the pipeline this reuses
- [Sans-IO IETF session](/quest/m1/rs2ts/sans-io/ietf.md) - the session shape it translates
- [IETF parameters](/quest/m1/rs2ts/ietf-params.md) - the concrete parameter codec it translates
26 changes: 14 additions & 12 deletions quest/m1/rs2ts/translator.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,23 @@ Mapping decided in planning:
`[Symbol.dispose]`; `Arc`/`Rc` of a type with drop glue become an explicit
refcount. JS is single-threaded, so `Mutex` and atomics become plain
access.
- Rust `u64` (and `VarInt` while it lasts) maps to js/net's `U64`
(`js/net/src/util/u64.ts`), read and written as a varint by
`Cursor.varint()` and `Writer.varint()`.
Integers up to 32 bits and `usize` map to `number` with checked arithmetic
that throws on overflow; never wrap silently. A `u64` or `i64` never maps
to a lossy `number`: the model accepts `u64::MAX` (e.g.
`model/subscription.rs`), so each one either becomes `U64` or an
`Option` in the source, or maps to a full-width 64-bit TypeScript type.
- Rust `u64` maps to js/net's `U64` (`js/net/src/util/u64.ts`, two `u32`
halves), generically, read and written as a varint by `Cursor.varint()` and
`Writer.varint()`. Integers up to 32 bits and `usize` map to `number` with
checked arithmetic that throws on overflow; never wrap silently. A `u64` or
`i64` never maps to a lossy `number`: the model accepts `u64::MAX` (e.g.
`model/subscription.rs`). Varint is a wire encoding in the codec, not a
type, so nothing maps by that name.

Guidance:

- Keep rs2ts a generic translator for a Rust subset, not a moq-net tool. It
maps by Rust type and construct (e.g. `u64` to one 64-bit TypeScript type),
never by moq-net names; anything project-specific lives in moq-net's source
or a small config, so another crate could use rs2ts unchanged.
- `varint::zigzag` and `unzigzag` (lite per-frame timestamps) still use
64-bit bit math, which the subset forbids. Rewrite them on two `u32` halves,
or give the 64-bit TypeScript type the operations they need.
- Pin Charon and its nightly in the nix shell for the regeneration lane only.
- Borrow rust-js's MIT oxc printer for formatting and source maps.
- Readability pass: inline single-use temporaries and keep source branch
Expand All @@ -54,7 +60,3 @@ Guidance:

Public API: none (internal tool). Lands on `dev` with the codec it
translates. Wire: none.

## Required

- [VarInt codec](/quest/m1/rs2ts/varint-codec.md) - the codec shape the translator targets
31 changes: 0 additions & 31 deletions quest/m1/rs2ts/varint-codec.md

This file was deleted.

2 changes: 1 addition & 1 deletion rs/hang/src/catalog/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use serde_with::{base64::Base64, serde_as};
/// rendition must be ignored by consumers.
#[derive(Debug, Clone, PartialEq, Default)]
pub enum Container {
/// A QUIC VarInt timestamp prefix followed by the raw codec payload.
/// A QUIC varint timestamp prefix followed by the raw codec payload.
/// Timestamps are in microseconds.
#[default]
Legacy,
Expand Down
14 changes: 6 additions & 8 deletions rs/hang/src/container/frame.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
use super::MAX_AGE;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use derive_more::Debug;
use moq_net::VarInt;

use crate::Error;

pub use moq_net::{Timescale, Timestamp};

/// Canonical timescale for the hang legacy wire format: microseconds.
///
/// The legacy container's on-wire timestamp is a single VarInt with no scale tag,
/// The legacy container's on-wire timestamp is a single varint with no scale tag,
/// so encoders normalize to this scale and decoders attach it.
pub const TIMESCALE: Timescale = Timescale::MICRO;

Expand Down Expand Up @@ -67,7 +66,7 @@ pub struct Frame {
}

impl Frame {
/// Encode the frame: VarInt timestamp prefix followed by the raw codec payload.
/// Encode the frame: varint timestamp prefix followed by the raw codec payload.
///
/// The timestamp is normalized to [`TIMESCALE`] (microseconds) so peers using a
/// different source scale (e.g. nanoseconds from MKV) can decode without knowing
Expand All @@ -78,12 +77,12 @@ impl Frame {
Ok(())
}

/// Decode a frame from raw bytes (VarInt timestamp prefix + payload).
/// Decode a frame from raw bytes (varint timestamp prefix + payload).
///
/// Attaches [`TIMESCALE`] (microseconds) to the decoded timestamp, matching what
/// [`Self::encode`] writes. Inverse of [`Self::encode`].
pub fn decode(mut buf: impl Buf) -> Result<Self, Error> {
let value: u64 = VarInt::decode_quic(&mut buf).map_err(moq_net::Error::from)?.into();
let value: u64 = moq_net::varint::decode_quic(&mut buf).map_err(moq_net::Error::from)?;
let timestamp = Timestamp::new(value, TIMESCALE)?;
let payload = buf.copy_to_bytes(buf.remaining());

Expand Down Expand Up @@ -117,11 +116,10 @@ impl Frame {
Ok(())
}

/// Write the VarInt timestamp prefix, normalized to [`TIMESCALE`].
/// Write the varint timestamp prefix, normalized to [`TIMESCALE`].
fn encode_header(&self, buf: &mut impl BufMut) -> Result<(), Error> {
let timestamp = self.timestamp.convert(TIMESCALE)?;
let value = VarInt::try_from(timestamp.value()).map_err(moq_net::Error::from)?;
value.encode_quic(buf).map_err(moq_net::Error::from)?;
moq_net::varint::encode_quic(timestamp.value(), buf).map_err(moq_net::Error::from)?;

Ok(())
}
Expand Down
7 changes: 3 additions & 4 deletions rs/moq-archive/src/segment.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::ops::RangeInclusive;

use bytes::{Buf, BufMut, Bytes, BytesMut};
use moq_net::VarInt;
use moq_net::varint;

use crate::path::{check_id, check_range};
use crate::{Error, Result, VERSION};
Expand Down Expand Up @@ -203,12 +203,11 @@ fn validate(groups: &[Group]) -> Result<()> {
}

fn write_varint(buf: &mut impl BufMut, value: u64) -> Result<()> {
let value = VarInt::try_from(value).map_err(|_| Error::Overflow)?;
value.encode_quic(buf).map_err(|_| Error::Overflow)
varint::encode_quic(value, buf).map_err(|_| Error::Overflow)
}

fn read_varint(buf: &mut impl Buf) -> Result<u64> {
Ok(VarInt::decode_quic(buf).map_err(|_| Error::Table)?.into_inner())
varint::decode_quic(buf).map_err(|_| Error::Table)
}

fn read_count(buf: &mut impl Buf, min_entry: usize) -> Result<usize> {
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-c/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use tracing::Level;
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug)]
pub enum moq_container_kind {
/// A QUIC VarInt timestamp prefix followed by the raw codec payload.
/// A QUIC varint timestamp prefix followed by the raw codec payload.
/// Timestamps are in microseconds.
MOQ_CONTAINER_KIND_LEGACY = 0,
/// Fragmented MP4: each frame is a complete moof+mdat fragment, described by
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-c/src/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,7 @@ pub unsafe extern "C" fn moq_decode_video_frame(id: u32, dst: *mut moq_video_fra
let frame = State::lock().video.frame(id)?;
let pixels = frame.pixels()?;
*dst = moq_video_frame {
// The decoded Timestamp is bounded by a QUIC VarInt, so its microseconds fit.
// The decoded Timestamp is bounded by a QUIC varint, so its microseconds fit.
timestamp_us: frame.frame.timestamp.as_micros() as u64,
width: pixels.width,
height: pixels.height,
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-ffi/src/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ pub struct MoqVideoDecodedFrame {
impl MoqVideoDecodedFrame {
/// Presentation timestamp, in microseconds.
pub fn timestamp_us(&self) -> u64 {
// A decoded Timestamp is bounded by a QUIC VarInt, so its microseconds fit.
// A decoded Timestamp is bounded by a QUIC varint, so its microseconds fit.
self.frame.timestamp.as_micros() as u64
}

Expand Down
20 changes: 10 additions & 10 deletions rs/moq-loc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
//! encode. Public properties are not handled here. They belong in the MoQ
//! object header and are stripped by the transport layer.
//!
//! Varint encoding is QUIC-style throughout via [`moq_net::VarInt`].
//! Varint encoding is QUIC-style throughout via [`moq_net::varint`].

use bytes::{Buf, Bytes, BytesMut};
use moq_net::{BoundsExceeded, DecodeError, EncodeError, VarInt};
use moq_net::{BoundsExceeded, DecodeError, EncodeError, varint};

/// Property IDs recognized by this implementation.
const PROP_TIMESCALE: u64 = 0x08;
Expand Down Expand Up @@ -104,7 +104,7 @@ impl From<EncodeError> for Error {
/// Consumes the properties_length prefix, walks the bounded property block,
/// and returns the remainder as `payload`.
pub fn decode(mut buf: Bytes) -> Result<Frame, Error> {
let properties_length: u64 = VarInt::decode_quic(&mut buf)?.into();
let properties_length = varint::decode_quic(&mut buf)?;
let properties_length: usize = properties_length.try_into().map_err(|_| Error::MalformedProperties)?;

if properties_length > buf.remaining() {
Expand All @@ -119,7 +119,7 @@ pub fn decode(mut buf: Bytes) -> Result<Frame, Error> {
let mut first = true;

while props.has_remaining() {
let delta: u64 = VarInt::decode_quic(&mut props)?.into();
let delta = varint::decode_quic(&mut props)?;
let abs = if first {
first = false;
delta
Expand All @@ -129,7 +129,7 @@ pub fn decode(mut buf: Bytes) -> Result<Frame, Error> {
prev_type = abs;

if abs % 2 == 0 {
let value: u64 = VarInt::decode_quic(&mut props)?.into();
let value = varint::decode_quic(&mut props)?;
match abs {
PROP_TIMESTAMP | PROP_TIMESTAMP_DRAFT03 => timestamp = Some(value),
PROP_TIMESCALE => {
Expand All @@ -141,7 +141,7 @@ pub fn decode(mut buf: Bytes) -> Result<Frame, Error> {
_ => {}
}
} else {
let len: u64 = VarInt::decode_quic(&mut props)?.into();
let len = varint::decode_quic(&mut props)?;
let len: usize = len.try_into().map_err(|_| Error::MalformedProperties)?;
if len > props.remaining() {
return Err(Error::MalformedProperties);
Expand All @@ -167,11 +167,11 @@ pub fn decode(mut buf: Bytes) -> Result<Frame, Error> {
/// catalog timescale to interpret `timestamp`.
pub fn encode(timestamp: u64, payload: &[u8]) -> Result<Bytes, Error> {
let mut props = BytesMut::with_capacity(16);
VarInt::try_from(PROP_TIMESTAMP)?.encode_quic(&mut props)?;
VarInt::try_from(timestamp)?.encode_quic(&mut props)?;
varint::encode_quic(PROP_TIMESTAMP, &mut props)?;
varint::encode_quic(timestamp, &mut props)?;

let mut out = BytesMut::with_capacity(props.len() + payload.len() + 8);
VarInt::try_from(props.len() as u64)?.encode_quic(&mut out)?;
varint::encode_quic(props.len() as u64, &mut out)?;
out.extend_from_slice(&props);
out.extend_from_slice(payload);

Expand All @@ -184,7 +184,7 @@ mod tests {

/// Test helper: write a u64 as a QUIC varint into `buf`.
fn write_varint(buf: &mut BytesMut, value: u64) {
VarInt::try_from(value).unwrap().encode_quic(buf).unwrap();
varint::encode_quic(value, buf).unwrap();
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-mux/src/catalog/hang/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::container::{Container as ContainerTrait, Frame, Kind, fmp4, legacy, l
///
/// Built from a track's audio or video configuration, including its container.
pub enum Container {
/// VarInt timestamp + raw codec bitstream. The original hang wire format.
/// varint timestamp + raw codec bitstream. The original hang wire format.
Legacy(Kind),
/// ISO-BMFF moof+mdat fragments. The wrapped [`fmp4::Wire`] holds
/// the track's `trak` box so per-frame writes and reads have the
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-mux/src/catalog/msf/consumer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ pub(crate) fn from_msf<E: CatalogExt>(msf: &moq_msf::Catalog<E>) -> Result<Catal
/// segment, and silently skipping it would mask a publisher bug.
fn container_from_msf(track: &moq_msf::Track) -> Result<Option<Container>> {
match &track.packaging {
// Neither is ISO-BMFF boxed, but they frame differently: a LOC property block against a VarInt
// Neither is ISO-BMFF boxed, but they frame differently: a LOC property block against a varint
// timestamp prefix. Reading one as the other misparses the head of every frame.
moq_msf::Packaging::Loc => Ok(Some(Container::Loc)),
moq_msf::Packaging::Legacy => Ok(Some(Container::Legacy)),
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-mux/src/container/legacy/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! The original hang wire format.
//!
//! Each moq frame holds one media frame: a VarInt-encoded timestamp
//! Each moq frame holds one media frame: a varint-encoded timestamp
//! followed by the raw codec bitstream. Simple but ad-hoc; new
//! broadcasts should use [`crate::container::loc`] instead.

Expand Down
6 changes: 6 additions & 0 deletions rs/moq-net/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ name = "announce"
harness = false
required-features = ["fuzz"]

# Reaches the private message codecs through the hidden `fuzz` module.
[[bench]]
name = "codec"
harness = false
required-features = ["fuzz"]

[[bench]]
name = "group"
harness = false
Expand Down
Loading
Loading