Skip to content
Merged
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 @@ -42,7 +42,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
14 changes: 8 additions & 6 deletions quest/m1/rs2ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ 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 stay 62-bit on the wire; the spec is not bounded to 2^53. Rust's
`VarInt` newtype carries Encode/Decode and JS gets a matching `VarInt` type
with checked conversion to and from `number`.
- 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
the package.
- The `@moq/net` API may change (disposable handles, `VarInt`) as long as it
- The `@moq/net` API may change (disposable handles, `U64`) as long as it
is no worse to use; watch, publish, hang, and the demos update in the same
change.
- Parity: `just test interop --all`, plus moq-net's own tests translated with
Expand All @@ -54,8 +57,7 @@ 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
- [JS VarInt](/quest/m1/rs2ts/js-varint.md) - js/net has a 62-bit `VarInt` type with checked `number` conversion and no BigInt on the hot path
- [JS VarInt](/quest/m1/rs2ts/js-varint.md) - js/net has a 64-bit `VarInt` type with checked `number` conversion and no BigInt on the hot path
- [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
Expand Down
2 changes: 1 addition & 1 deletion quest/m1/rs2ts/ietf.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ the hand-written js/net IETF code (about 8.7k lines) is deleted, with
## Plan

Values above 2^53 are legal on the IETF wire (request ids, track aliases);
they stay exact as `VarInt` and only fail where code converts them to
they stay exact as `U64` and only fail where code converts them to
`number`.

Public API: breaks `@moq/net`; retargets to `dev`. Wire: none.
Expand Down
7 changes: 4 additions & 3 deletions quest/m1/rs2ts/js-varint.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Goal

js/net has a `VarInt` type that holds the full 62-bit range, converts to and
js/net has a `VarInt` type that holds the full 64-bit range, converts to and
from `number` with a loud error outside the safe range, and encodes and
decodes without BigInt on the hot path. It is the TypeScript type rs2ts maps
Rust's `VarInt` to.
Expand All @@ -21,7 +21,8 @@ Guidance:
and comparison and increment methods so sequence logic never converts.
- Move js/net's varint reading and writing onto it, dropping the BigInt
round trip for QUIC and leading-ones varints.
- Unit-test the boundaries (2^30, 2^53, 2^62 - 1) against Rust's encoder in
`just test interop`.
- Unit-test the boundaries (2^30, 2^53, 2^62 - 1, 2^62, 2^64 - 1) against
Rust's encoder in `just test interop`. The QUIC form refuses anything past
2^62 - 1, as Rust's does.

Public API: additive to `@moq/net`; lands on `main`. Wire: none.
2 changes: 1 addition & 1 deletion quest/m1/rs2ts/lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ first-frame latency are no worse than the hand-written js/net.
## Plan

- The `@moq/net` API may change where the generated shape is no worse to
use: disposable handles (`using`), `VarInt` for sequences and ids. Update
use: disposable handles (`using`), `U64` for sequences and ids. Update
watch, publish, hang, room, and the demos in the same change, and the
`doc/` pages for anything user-facing.
- A forgotten `drop()` leaves a track open forever: add a debug-only
Expand Down
17 changes: 8 additions & 9 deletions quest/m1/rs2ts/translator.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,20 @@ 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 `VarInt` maps to the [JS VarInt](/quest/m1/rs2ts/js-varint.md) type.
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 `VarInt` or an
`Option` in the source, or maps to a full-width 64-bit TypeScript type.
- Rust `u64` maps to a TypeScript `U64` (two `u32` halves), generically; see
[JS U64](/quest/m1/rs2ts/js-varint.md). 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::from_zigzag` and `to_zigzag` (lite per-frame timestamps) still use
- `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.
Expand All @@ -62,5 +62,4 @@ translates. Wire: none.

## Required

- [VarInt codec](/quest/m1/rs2ts/varint-codec.md) - the codec shape the translator targets
- [JS VarInt](/quest/m1/rs2ts/js-varint.md) - the TypeScript type `VarInt` maps to
- [JS U64](/quest/m1/rs2ts/js-varint.md) - the TypeScript `U64` that Rust `u64` maps to
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 @@ -621,7 +621,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