feat(net): BigInt-free varint codec, internal U64, and checked Varint.decode - #4454
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Quest outcome: implemented, left as a draft for the maintainer's API call. Local checks: Open decisions:
(Written by Claude Opus 5.5) |
…t 2^53 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (23)
💤 Files with no reviewable changes (1)
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review. WalkthroughThe change adds checked Priority: ➖ Normal Severity of issue fixed: Low Merge Risk: ⚪ Minimal · up to The varint changes are ready for normal checks; no actionable merge-blocking issue remains identified. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The changed decoding paths retain format selection, bounded read requests, and checks against unsafe number conversion. No newly exposed security failure was established, though the behavior of future callers using full-width values will depend on their field-specific checks. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Merge summary. Reviewed the codec, Decisions taken:
Ordering with related PRs:
(written by Claude Opus 5.5) |
Problem
js/net converted every varint through BigInt:
encodeToandencodeLeadingOnesTocalledBigInt(v)even for a 1-byte value, and every leading-ones decode and every 8-byte QUIC decode went BigInt thenNumber(). The rs2ts line also needs a TypeScript type for Rust'su64that holds more than 2^53 without BigInt on the hot path. AndVarint.decodesilently rounded values above 2^53.Quest:
quest/m1/rs2ts/js-varint.md(deleted here, per the questline's explicit exception that lets this additive piece land onmain).Approach
js/net/src/util/varint.ts(new, package-internal): the QUIC and leading-ones codec on two u32 halves. Decoders return the low half and leave the high half in a module scratch, so a read allocates nothing. Encoders size first, then write into an exact-size view, one allocation per encode as before.js/net/src/util/u64.ts(new, package-internal) holdsU64, a generic unsigned 64-bit integer as an immutable pair of u32 halves. Varints are only its wire encoding. rs2ts will map every Rustu64to it.fromNumber/toNumberthrow above 2^53 - 1, or on negative or fractional input.fromBigInt/toBigIntspan the full u64 range.compare,equals,add(delta)(throws on overflow),toString,ZERO, andMAX.0xFFform. The QUIC form can't hold the top of the range, so its encoder throws there rather than truncating.Cursor.u53for both formats. Their 1 and 2-byte forms share a shape: a 1-byte varint is the byte, and a 2-byte one is the low 6 bits plus the next byte. Only the first-byte bound moves (0x40/0x80 for QUIC, 0x80/0xc0 for leading-ones). A third tier covers the rest of the forms up to 4 bytes (QUIC's 30 bits, leading-ones' 28), which need no upper half. Everything longer takes the general decode. The Cursor picks the format once, in its constructor, throughisLeadingOnes(version). feat(lite): moq-lite-07 switches to 64-bit leading-ones varints #4455 (lite-07 to leading-ones) can drop its ownu53fast path on rebase and only widenisLeadingOnes.Cursor/Reader/Writer(internalstream.ts):u53/u62run on the halves codec, with no BigInt untilu62hands back its bigint. Newvarint()reads and writes a varint as aU64, which is what rs2ts will emit.toNumberinutil/varint.ts):Varint.decode,Cursor.u53, andU64.toNumber.@moq/locskips unknown even-typed properties withdecodeBigInt, since their values may not fit anumber. The timestamp and timescale properties still decode as numbers and now throw past 2^53 - 1.@moq/hang's legacy container throws on a timestamp past 2^53 - 1 instead of rounding it.varint_interopin moq-net hands Rust's QUIC and leading-ones encodings of every size boundary (plus the 2^53 edge of anumber, and Rust's largest encodable value) totest/interop/varint.ts. That script decodes them intoU64, checks thenumberconversion, and returns js/net's encodings, which Rust requires to match byte for byte. It runs first injust test interop, and a mutated JS encoder fails it. Values Rust can't yet represent are covered by JS-only tests.Impact
@moq/net): no new exports. TheVarintnamespace doc comment now names both formats.Varint.decodethrows aRangeError("value larger than 53-bits: N") for values above 2^53 - 1, where it used to round. UseVarint.decodeBigIntfor those.Reader.u53already threw; its error is now aRangeErrortoo.Varint.*encoders throwRangeErrorinstead ofErroron overflow (same messages), and throw on fractional input before any BigInt conversion.@moq/loc: a frame with an unknown property above 2^53 now decodes instead of silently rounding the skipped value. A timestamp or timescale above 2^53 - 1 throws.@moq/hang(legacy container): a timestamp above 2^53 - 1 throws.Cursor.varint(),Reader.varint(),Writer.varint(), andU64inutil/u64.ts.bun build --minify --target browserof@moq/net, gzip -9): 85,762 B -> 86,017 B (+255 B, +0.3%).Benchmarks
Only browser performance matters, so node (V8, as in Chrome) is the primary signal and bun (JavaScriptCore) is secondary.
Same machine, main vs branch, alternating runs (8 per side), min of each run's 9 reps, ns per varint. From
js/net/bench/varint.ts(new, wired into nightly), which warms every case before timing so the first row doesn't measure JIT warmup. Each format is measured at the largestnumbervalue of its own 1, 2, 4, and 8-byte forms. Decode goes throughCursor.u53(), encode through the publicencodeTo/encodeLeadingOnesTo.On node, every 1 and 2-byte row matches or beats main. Bun's QUIC 2-byte decode is 0.5 ns (about 10%) slower; that gap is accepted.
End to end,
js/net/bench/frames.ts(bun, median of 3 alternating runs, ns per frame through a group stream into a reader):Lite rows land between -13% and +23% from run to run, the same spread an earlier run showed with main's exact lite decode code. The per-varint numbers above are the reliable signal for lite.
Public API tradeoffs
Decided:
U64stays internal. Nothing in watch, publish, hang, or the demos needs it, and rs2ts output lives inside js/net. Exporting it later is additive onmain, but un-exporting would be a break.U64class (u32 halves)u64maps to it one to one.varint()read. Values needtoNumber()at the edge.number+ bigint fallback (a union type everywhere, silent rounding risk). A{hi, lo}object literal (same cost, no methods or validation).biginteverywhere (the "main" columns above: 10-40x slower).fromNumber/toNumber(throw past 2^53 - 1)Number()silently rounded.Varint.decodenow shares the check (see Impact). hang/loc updated.fromBigInt/toBigIntu62callers and tests use the bigint form.compare/equals/addincrement()only (does not cover +2). Operators viavalueOf(silently lossy, rejected).Cursor/Reader/Writer.varint()u53/u62are unchanged.u53,u62,varint).stream.tsis not exported)bigintfields toU64now (large churn in code the generated IETF session replaces).Varint.decodethrowing past 2^53 - 1Alternatives
U64from@moq/netnow (the quest's original "additive" plan): rejected until a consumer needs it.VarInttype: namedU64instead, since rs2ts stays generic and maps every Rustu64(including the one inside Rust'sVarInt) to it.Follow-ups
VarIntto 64 bits. Once it lands, extendvarint_interopto 2^62 and 2^64 - 1 (QUIC refusing past 2^62 - 1 on both sides).Cursor.u53fast path. This one covers leading-ones up to 28 bits.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code