From 03024c781e3d71523c41f5b60706e768f3797d03 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 17:48:19 -0700 Subject: [PATCH 1/4] chore(lint): fail checks on Biome and clippy.toml warnings Biome's recommended rules default to warn and `biome check` exits 0 on warnings, so 15 crept into js/net. Fix them and pass --error-on-warnings. The wasm32 clippy lane builds tokio without `time`, so the tokio::time entries in moq-mux and moq-ffi clippy.toml did not resolve. Mark them allow-invalid. Co-Authored-By: Claude Opus 5.5 --- js/net/bench/refresh.ts | 9 ++++++--- js/net/src/connection/connect.ts | 6 +++--- js/net/src/lite/refresh.test.ts | 13 ++++++++----- js/net/src/priority.test.ts | 8 ++++++-- js/net/src/track.ts | 2 +- rs/moq-ffi/clippy.toml | 11 ++++++----- rs/moq-mux/clippy.toml | 11 ++++++----- sh/js/workspace.py | 3 ++- sh/js/workspace.test.py | 13 +++++++------ 9 files changed, 45 insertions(+), 31 deletions(-) diff --git a/js/net/bench/refresh.ts b/js/net/bench/refresh.ts index 1cfccd18b8c..3771c260d54 100644 --- a/js/net/bench/refresh.ts +++ b/js/net/bench/refresh.ts @@ -32,14 +32,16 @@ for (const retained of [1, 64, 1024]) while ((await announcements.next())?.kind !== "start") {} const request = destination.request(Path.from("room")); while (!request.active.peek()) await request.active.changed(); - const remote = request.active.peek()!; + const remote = request.active.peek(); + if (!remote) throw new Error("broadcast missing"); const consumers = producers.map((_, i) => remote.track(`data${i}`)); const baseline = consumers.map((track) => track.subscribe()); for (let i = 0; i < tracks; i++) { const group = producers[i].appendGroup(); group.writeFrame({ payload: new Uint8Array(3) }); group.close(); - const received = (await baseline[i].recvGroup())!; + const received = await baseline[i].recvGroup(); + if (!received) throw new Error("baseline group missing"); await received.readFrame(); received.close(); } @@ -61,7 +63,8 @@ for (const retained of [1, 64, 1024]) group.writeFrame({ payload: new Uint8Array(3) }); group.writeFrame({ payload: new Uint8Array(4) }); group.close(); - const received = (await baseline[0].recvGroup())!; + const received = await baseline[0].recvGroup(); + if (!received) throw new Error("measured group missing"); await received.readFrame(); await received.readFrame(); received.close(); diff --git a/js/net/src/connection/connect.ts b/js/net/src/connection/connect.ts index e4b56b00f87..119e1fa8b69 100644 --- a/js/net/src/connection/connect.ts +++ b/js/net/src/connection/connect.ts @@ -348,12 +348,12 @@ async function connectTransport( try { return await Promise.race([terminal, negotiate(url, session, wiring, setup)]); - } catch (cause) { + } catch (failure) { // We refused the peer's SETUP and already closed with the code that says why. - if (cause instanceof ProtocolViolation) throw cause; + if (failure instanceof ProtocolViolation) throw failure; // A session failure already guarantees a terminal close; its authoritative code // must not be overwritten by the reset-only fallback while the browser delivers it. - cause = await sessionCause(session, cause); + const cause = await sessionCause(session, failure); // A session shutdown can reject its SETUP stream before `closed` publishes the // peer's close code. Wait briefly without closing locally, since a local clean close // could overwrite that code. A reset-only peer is cleaned up after the grace period. diff --git a/js/net/src/lite/refresh.test.ts b/js/net/src/lite/refresh.test.ts index 45ac727154f..bc9b43a98df 100644 --- a/js/net/src/lite/refresh.test.ts +++ b/js/net/src/lite/refresh.test.ts @@ -40,21 +40,24 @@ test.each([ALPN_06, ALPN_07_WIP])("refresh joins reach a browser publisher on %s while ((await announcements.next())?.kind !== "start") {} const request = b.request(Path.from("room")); while (request.active.peek() === undefined) await request.active.changed(); - const remote = request.active.peek()!.track("data"); + const active = request.active.peek(); + if (!active) throw new Error("broadcast missing"); + const remote = active.track("data"); expect((await remote.info()).refresh).toBe(alpn === ALPN_07_WIP ? 1 : undefined); const baseline = remote.subscribe(); const group = producer.appendGroup(); group.writeFrame({ payload: new Uint8Array(3) }); group.writeFrame({ payload: new Uint8Array(3) }); const received = await baseline.recvGroup(); - expect((await received!.readFrame())!.payload.length).toBe(3); - expect((await received!.readFrame())!.payload.length).toBe(3); + if (!received) throw new Error("group missing"); + expect((await received.readFrame())?.payload.length).toBe(3); + expect((await received.readFrame())?.payload.length).toBe(3); const joins = [remote.subscribe()]; await clock.advanceAsync(Nano(0)); expect(producer.demand().wanted.peek()).toBe(0); updates.length = 0; group.writeFrame({ payload: new Uint8Array(1) }); - expect((await received!.readFrame())!.payload.length).toBe(1); + expect((await received.readFrame())?.payload.length).toBe(1); for (let i = 0; i < 8; i++) joins.push(remote.subscribe()); await clock.advanceAsync(Nano(0)); if (alpn === ALPN_07_WIP) { @@ -68,7 +71,7 @@ test.each([ALPN_06, ALPN_07_WIP])("refresh joins reach a browser publisher on %s await clock.advanceAsync(Nano(0)); expect(updates).toEqual(alpn === ALPN_07_WIP ? [1, 1] : []); for (const join of joins) join.close(); - received!.close(); + received.close(); baseline.close(); group.close(); } finally { diff --git a/js/net/src/priority.test.ts b/js/net/src/priority.test.ts index 1a38e24175c..b71563bb9d1 100644 --- a/js/net/src/priority.test.ts +++ b/js/net/src/priority.test.ts @@ -130,8 +130,12 @@ test.each(["ascending", "descending"] as const)("a fill outranks its group's liv priority.add(rest, 5, 2 ** 45); priority.add(fill, 5, 0); priority.add(other, order === "ascending" ? 4 : 6, Number.MAX_SAFE_INTEGER); - expect(streams[1].sendOrder).toBeGreaterThan(streams[0].sendOrder!); - expect(streams[2].sendOrder).toBeGreaterThan(streams[1].sendOrder!); + const [restOrder, fillOrder, otherOrder] = streams.map((stream) => { + if (stream.sendOrder === undefined) throw new Error("send order missing"); + return stream.sendOrder; + }); + expect(fillOrder).toBeGreaterThan(restOrder); + expect(otherOrder).toBeGreaterThan(fillOrder); priority.remove(fill); expect(streams[0].sendOrder).toBe(sendOrder({ priority: 7, position: 1 })); priority.close(); diff --git a/js/net/src/track.ts b/js/net/src/track.ts index d6f11592e30..1297d59a80d 100644 --- a/js/net/src/track.ts +++ b/js/net/src/track.ts @@ -2160,7 +2160,7 @@ export class Subscriber { } for (let run = this.#pending.shift(); run; run = this.#pending.shift()) { const dropped = report(run.start, run.end); - if (!dropped || dropped.kind !== "drop") continue; + if (dropped?.kind !== "drop") continue; if (dropped.end < run.end) this.#pending.unshift({ start: dropped.end, end: run.end }); return dropped; } diff --git a/rs/moq-ffi/clippy.toml b/rs/moq-ffi/clippy.toml index 892532bf144..db1d0ee8dbe 100644 --- a/rs/moq-ffi/clippy.toml +++ b/rs/moq-ffi/clippy.toml @@ -1,9 +1,10 @@ +# The wasm32 lint builds tokio without `time`, where these tokio paths do not resolve. disallowed-methods = [ { path = "std::time::Instant::now", reason = "read the ambient clock: moq_clock::now()" }, { path = "std::time::SystemTime::now", reason = "read the ambient clock: moq_clock::wall()" }, - { path = "tokio::time::Instant::now", reason = "read the ambient clock: moq_clock::now()" }, - { path = "tokio::time::sleep", reason = "sleep on the ambient clock: moq_clock::sleep()" }, - { path = "tokio::time::sleep_until", reason = "sleep on the ambient clock: moq_clock::sleep_until()" }, - { path = "tokio::time::timeout", reason = "use the ambient clock: moq_clock::timeout()" }, - { path = "tokio::time::interval", reason = "use the ambient clock: moq_clock::interval()" }, + { path = "tokio::time::Instant::now", reason = "read the ambient clock: moq_clock::now()", allow-invalid = true }, + { path = "tokio::time::sleep", reason = "sleep on the ambient clock: moq_clock::sleep()", allow-invalid = true }, + { path = "tokio::time::sleep_until", reason = "sleep on the ambient clock: moq_clock::sleep_until()", allow-invalid = true }, + { path = "tokio::time::timeout", reason = "use the ambient clock: moq_clock::timeout()", allow-invalid = true }, + { path = "tokio::time::interval", reason = "use the ambient clock: moq_clock::interval()", allow-invalid = true }, ] diff --git a/rs/moq-mux/clippy.toml b/rs/moq-mux/clippy.toml index 892532bf144..db1d0ee8dbe 100644 --- a/rs/moq-mux/clippy.toml +++ b/rs/moq-mux/clippy.toml @@ -1,9 +1,10 @@ +# The wasm32 lint builds tokio without `time`, where these tokio paths do not resolve. disallowed-methods = [ { path = "std::time::Instant::now", reason = "read the ambient clock: moq_clock::now()" }, { path = "std::time::SystemTime::now", reason = "read the ambient clock: moq_clock::wall()" }, - { path = "tokio::time::Instant::now", reason = "read the ambient clock: moq_clock::now()" }, - { path = "tokio::time::sleep", reason = "sleep on the ambient clock: moq_clock::sleep()" }, - { path = "tokio::time::sleep_until", reason = "sleep on the ambient clock: moq_clock::sleep_until()" }, - { path = "tokio::time::timeout", reason = "use the ambient clock: moq_clock::timeout()" }, - { path = "tokio::time::interval", reason = "use the ambient clock: moq_clock::interval()" }, + { path = "tokio::time::Instant::now", reason = "read the ambient clock: moq_clock::now()", allow-invalid = true }, + { path = "tokio::time::sleep", reason = "sleep on the ambient clock: moq_clock::sleep()", allow-invalid = true }, + { path = "tokio::time::sleep_until", reason = "sleep on the ambient clock: moq_clock::sleep_until()", allow-invalid = true }, + { path = "tokio::time::timeout", reason = "use the ambient clock: moq_clock::timeout()", allow-invalid = true }, + { path = "tokio::time::interval", reason = "use the ambient clock: moq_clock::interval()", allow-invalid = true }, ] diff --git a/sh/js/workspace.py b/sh/js/workspace.py index 42d4cee0210..14750a61c9f 100644 --- a/sh/js/workspace.py +++ b/sh/js/workspace.py @@ -181,7 +181,8 @@ def scripts(verb): finally: Path(name).unlink() # The fallback also covers unowned inputs, including demo/pub's Worker. - command("bun", "biome", "check", *(["js", "bench/browser", "test", "doc", "demo"] if plan["all"] else + # Recommended rules default to warn, which Biome otherwise lets pass. + command("bun", "biome", "check", "--error-on-warnings", *(["js", "bench/browser", "test", "doc", "demo"] if plan["all"] else [p["directory"] for p in packages.values()] + ["js/common"])) scripts("build") for name in ("@moq/watch", "@moq/publish"): diff --git a/sh/js/workspace.test.py b/sh/js/workspace.test.py index ccd06738064..d94196906fb 100644 --- a/sh/js/workspace.test.py +++ b/sh/js/workspace.test.py @@ -191,16 +191,17 @@ def test_executor_runs_only_selected_package_scripts_without_wasm(self): def test_biome_covers_unowned_js_files_in_fallback(self): def biome(paths): return next(c for c in self.calls(paths) if c[:3] == ["bun", "biome", "check"]) - self.assertEqual(biome(["js/tsconfig.json"]), ["bun", "biome", "check", "js", "test", "doc", "demo"]) - self.assertEqual(biome(["doc/index.md"]), ["bun", "biome", "check", "doc", "js/common"]) + self.assertEqual(biome(["js/tsconfig.json"]), + ["bun", "biome", "check", "--error-on-warnings", "js", "bench/browser", "test", "doc", "demo"]) + self.assertEqual(biome(["doc/index.md"]), ["bun", "biome", "check", "--error-on-warnings", "doc", "js/common"]) self.assertEqual(biome(["demo/nested/src/index.ts"]), - ["bun", "biome", "check", "demo/nested", "js/common"]) + ["bun", "biome", "check", "--error-on-warnings", "demo/nested", "js/common"]) self.assertEqual(biome(["demo/pub/src/index.ts"]), - ["bun", "biome", "check", "js", "test", "doc", "demo"]) + ["bun", "biome", "check", "--error-on-warnings", "js", "bench/browser", "test", "doc", "demo"]) self.assertEqual(biome(["js/core/src/index.ts"]), - ["bun", "biome", "check", "js/core", "js/view", "doc", "js/common"]) + ["bun", "biome", "check", "--error-on-warnings", "js/core", "js/view", "doc", "js/common"]) self.assertEqual(biome(["js/wasm/src/index.ts"]), - ["bun", "biome", "check", "js/wasm", "test/browser", "demo/nested", "js/common"]) + ["bun", "biome", "check", "--error-on-warnings", "js/wasm", "test/browser", "demo/nested", "js/common"]) def test_build_never_generates_wasm(self): self.assertNotIn(["just", "js", "wasm"], self.calls(None, "build")) From 67a6bae6837dda9746c0432723797073464946a1 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 19:36:08 -0700 Subject: [PATCH 2/4] chore(lint): fail Sphinx and OBS builds on warnings, fix Vite config notices Sphinx: json.py annotated tracks with bare TrackProducer/TrackConsumer, which match three documented classes. Qualify them by module like flate.py, and build with -W --keep-going locally and fail_on_warning on Read the Docs. OBS: the source test includes moq-source.cpp, so GCC treated it as a header and flagged moq_source holding anonymous-namespace types. Move moq_source into the namespace, and compile the plugin and tests with -Werror. Vite: use import.meta.dirname and explicit .ts extensions in every config, which the native config loader requires. Co-Authored-By: Claude Opus 5.5 --- .readthedocs.yaml | 1 + cpp/obs/src/moq-source.cpp | 8 ++++---- demo/boy/vite.config.ts | 8 ++++---- demo/web/vite.config.ts | 18 +++++++++--------- js/moq-boy/vite.config.ts | 4 ++-- js/publish/vite.config.ts | 14 +++++++------- js/watch/vite.config.ts | 12 ++++++------ py/justfile | 2 +- py/moq-rs/moq/json.py | 12 ++++++------ sh/obs/compile.sh | 3 ++- sh/obs/unit.sh | 2 +- test/audio-quality/clients/js/vite.config.ts | 2 +- test/interop/clients/js/vite.config.ts | 2 +- 13 files changed, 45 insertions(+), 43 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 98ea9f81d4e..0b410d55cd9 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -18,6 +18,7 @@ python: sphinx: configuration: py/moq-rs/docs/conf.py + fail_on_warning: true formats: - htmlzip diff --git a/cpp/obs/src/moq-source.cpp b/cpp/obs/src/moq-source.cpp index 1f4e9a54871..b0ecaa6e723 100644 --- a/cpp/obs/src/moq-source.cpp +++ b/cpp/obs/src/moq-source.cpp @@ -70,10 +70,10 @@ static AVCodecID codec_string_to_id(const char *codec, size_t len) return AV_CODEC_ID_NONE; } -struct moq_source; - namespace { +struct moq_source; + enum class Decoded { Pending, Ready, Failed }; // A track owns its codec and converted output, including while it is only a candidate. @@ -158,8 +158,6 @@ struct Connection { std::shared_ptr pending_audio; }; -} // namespace - struct moq_source { obs_source_t *source = nullptr; @@ -184,6 +182,8 @@ struct moq_source { MoQWorker worker; }; +} // namespace + // Forward declarations static void moq_source_update(void *data, obs_data_t *settings); static void moq_source_destroy(void *data); diff --git a/demo/boy/vite.config.ts b/demo/boy/vite.config.ts index 176ac96ef3d..6d21eec5b13 100644 --- a/demo/boy/vite.config.ts +++ b/demo/boy/vite.config.ts @@ -1,18 +1,18 @@ import { resolve } from "path"; import { defineConfig } from "vite"; import solidPlugin from "vite-plugin-solid"; -import { crossOriginIsolation } from "../../js/common/vite-plugin-isolate"; -import { worklet } from "../../js/common/vite-plugin-worklet"; +import { crossOriginIsolation } from "../../js/common/vite-plugin-isolate.ts"; +import { worklet } from "../../js/common/vite-plugin-worklet.ts"; export default defineConfig({ root: "src", - envDir: resolve(__dirname), + envDir: resolve(import.meta.dirname), plugins: [solidPlugin(), worklet(), crossOriginIsolation()], build: { target: "esnext", rollupOptions: { input: { - main: resolve(__dirname, "src/index.html"), + main: resolve(import.meta.dirname, "src/index.html"), }, }, }, diff --git a/demo/web/vite.config.ts b/demo/web/vite.config.ts index 850750b3598..7b6961e971f 100644 --- a/demo/web/vite.config.ts +++ b/demo/web/vite.config.ts @@ -2,14 +2,14 @@ import tailwindcss from "@tailwindcss/vite"; import { resolve } from "path"; import { defineConfig } from "vite"; import solidPlugin from "vite-plugin-solid"; -import { crossOriginIsolation } from "../../js/common/vite-plugin-isolate"; -import { worklet } from "../../js/common/vite-plugin-worklet"; -import { consoleOverlay } from "./console-overlay"; -import { openTabs } from "./open-tabs"; +import { crossOriginIsolation } from "../../js/common/vite-plugin-isolate.ts"; +import { worklet } from "../../js/common/vite-plugin-worklet.ts"; +import { consoleOverlay } from "./console-overlay.ts"; +import { openTabs } from "./open-tabs.ts"; export default defineConfig({ root: "src", - envDir: resolve(__dirname), + envDir: resolve(import.meta.dirname), plugins: [ tailwindcss(), solidPlugin(), @@ -25,10 +25,10 @@ export default defineConfig({ sourcemap: process.env.NODE_ENV === "production" ? false : "inline", rollupOptions: { input: { - watch: resolve(__dirname, "src/watch.html"), - publish: resolve(__dirname, "src/publish.html"), - stats: resolve(__dirname, "src/stats.html"), - meet: resolve(__dirname, "src/meet.html"), + watch: resolve(import.meta.dirname, "src/watch.html"), + publish: resolve(import.meta.dirname, "src/publish.html"), + stats: resolve(import.meta.dirname, "src/stats.html"), + meet: resolve(import.meta.dirname, "src/meet.html"), }, }, }, diff --git a/js/moq-boy/vite.config.ts b/js/moq-boy/vite.config.ts index ef8ee6ed088..e1098817080 100644 --- a/js/moq-boy/vite.config.ts +++ b/js/moq-boy/vite.config.ts @@ -7,8 +7,8 @@ export default defineConfig({ build: { lib: { entry: { - index: resolve(__dirname, "src/index.ts"), - element: resolve(__dirname, "src/element.tsx"), + index: resolve(import.meta.dirname, "src/index.ts"), + element: resolve(import.meta.dirname, "src/element.tsx"), }, formats: ["es"], }, diff --git a/js/publish/vite.config.ts b/js/publish/vite.config.ts index 6fee12be92b..15ff62bd717 100644 --- a/js/publish/vite.config.ts +++ b/js/publish/vite.config.ts @@ -1,18 +1,18 @@ import { resolve } from "path"; import { defineConfig } from "vite"; -import { worklet } from "../common/vite-plugin-worklet"; -import { lazyMedia } from "./bundle"; +import { worklet } from "../common/vite-plugin-worklet.ts"; +import { lazyMedia } from "./bundle.ts"; export default defineConfig({ plugins: [worklet(), lazyMedia()], build: { lib: { entry: { - index: resolve(__dirname, "src/index.ts"), - element: resolve(__dirname, "src/element.ts"), - "ui/element": resolve(__dirname, "src/ui/element.ts"), - "support/index": resolve(__dirname, "src/support/index.ts"), - "support/element": resolve(__dirname, "src/support/element.ts"), + index: resolve(import.meta.dirname, "src/index.ts"), + element: resolve(import.meta.dirname, "src/element.ts"), + "ui/element": resolve(import.meta.dirname, "src/ui/element.ts"), + "support/index": resolve(import.meta.dirname, "src/support/index.ts"), + "support/element": resolve(import.meta.dirname, "src/support/element.ts"), }, formats: ["es"], }, diff --git a/js/watch/vite.config.ts b/js/watch/vite.config.ts index 973d0b3a075..47a979f9b95 100644 --- a/js/watch/vite.config.ts +++ b/js/watch/vite.config.ts @@ -1,17 +1,17 @@ import { resolve } from "path"; import { defineConfig } from "vite"; -import { worklet } from "../common/vite-plugin-worklet"; +import { worklet } from "../common/vite-plugin-worklet.ts"; export default defineConfig({ plugins: [worklet()], build: { lib: { entry: { - index: resolve(__dirname, "src/index.ts"), - element: resolve(__dirname, "src/element.ts"), - "ui/element": resolve(__dirname, "src/ui/element.ts"), - "support/index": resolve(__dirname, "src/support/index.ts"), - "support/element": resolve(__dirname, "src/support/element.ts"), + index: resolve(import.meta.dirname, "src/index.ts"), + element: resolve(import.meta.dirname, "src/element.ts"), + "ui/element": resolve(import.meta.dirname, "src/ui/element.ts"), + "support/index": resolve(import.meta.dirname, "src/support/index.ts"), + "support/element": resolve(import.meta.dirname, "src/support/element.ts"), }, formats: ["es"], }, diff --git a/py/justfile b/py/justfile index de25dcbb553..889942e10bb 100644 --- a/py/justfile +++ b/py/justfile @@ -89,4 +89,4 @@ package: docs: rm -rf dist/docs uv run --isolated --locked --only-group docs \ - sphinx-build -b html moq-rs/docs dist/docs + sphinx-build -b html -W --keep-going moq-rs/docs dist/docs diff --git a/py/moq-rs/moq/json.py b/py/moq-rs/moq/json.py index 434d4bc5ea5..f0734e32cd4 100644 --- a/py/moq-rs/moq/json.py +++ b/py/moq-rs/moq/json.py @@ -23,10 +23,10 @@ MoqJsonValue, ) +from . import publish, subscribe from ._records import _opt_from_us, _opt_us from ._stream import _Stream -from .publish import BroadcastProducer, TrackDemand, TrackProducer -from .subscribe import TrackConsumer +from .publish import BroadcastProducer, TrackDemand __all__ = ["SnapshotConsumer", "SnapshotProducer", "StreamConsumer", "StreamProducer", "Timed"] @@ -63,7 +63,7 @@ class SnapshotProducer: def __init__( self, broadcast: BroadcastProducer, - track: TrackProducer, + track: publish.TrackProducer, *, delta_ratio: int | None = None, compression: bool = False, @@ -97,7 +97,7 @@ class StreamProducer: carries is refused. """ - def __init__(self, broadcast: BroadcastProducer, track: TrackProducer, *, compression: bool = False) -> None: + def __init__(self, broadcast: BroadcastProducer, track: publish.TrackProducer, *, compression: bool = False) -> None: config = MoqJsonStreamConfig(compression=compression) self._inner = MoqJsonStreamProducer(broadcast._inner, track._inner, config) @@ -123,7 +123,7 @@ class SnapshotConsumer(_Stream[Timed]): context manager that cancels on exit. """ - def __init__(self, track: TrackConsumer, *, compression: bool = False) -> None: + def __init__(self, track: subscribe.TrackConsumer, *, compression: bool = False) -> None: # delta_ratio is producer-only, so leave it at its default here. config = MoqJsonSnapshotConfig(compression=compression) self._inner = MoqJsonSnapshotConsumer(track._inner, config) @@ -150,7 +150,7 @@ class StreamConsumer(_Stream[Timed]): async context manager that cancels on exit. """ - def __init__(self, track: TrackConsumer, *, compression: bool = False) -> None: + def __init__(self, track: subscribe.TrackConsumer, *, compression: bool = False) -> None: config = MoqJsonStreamConfig(compression=compression) self._inner = MoqJsonStreamConsumer(track._inner, config) diff --git a/sh/obs/compile.sh b/sh/obs/compile.sh index d7c5449a97d..09aeaf5595e 100755 --- a/sh/obs/compile.sh +++ b/sh/obs/compile.sh @@ -52,11 +52,12 @@ ffmpeg=$(pkg-config --cflags libavcodec libavutil libswscale libswresample) status=0 for source in src/*.cpp test/*.cpp; do # shellcheck disable=SC2086 + # -Werror: a warning that only prints is never fixed. # -Wno-unused-command-line-argument: the nix compiler wrapper injects # link flags, which a syntax-only run reports as unused, once per file. # $qt and $ffmpeg stay unquoted on purpose: pkg-config hands back one # space-separated string, and splitting it is the intended reading. - if ! "$cxx" -std=c++17 -fsyntax-only -Wno-unused-command-line-argument \ + if ! "$cxx" -std=c++17 -fsyntax-only -Werror -Wno-unused-command-line-argument \ "${includes[@]}" $qt $ffmpeg \ -DMOQ_FRONTEND_ENABLED -DMOQ_VERSION_STRING='"0.0.0"' "$source"; then status=1 diff --git a/sh/obs/unit.sh b/sh/obs/unit.sh index cfc3770cfc5..50c4b0a215e 100755 --- a/sh/obs/unit.sh +++ b/sh/obs/unit.sh @@ -35,7 +35,7 @@ export PKG_CONFIG_PATH="$moq_prefix/lib/pkgconfig" moq_sources=$(pkg-config --variable=sources moq-cpp) moq_libs=$(pkg-config --libs moq-cpp) -flags=(-std=c++17 -g -O0 -pthread) +flags=(-std=c++17 -g -O0 -pthread -Werror) if [ "$mode" = "tsan" ]; then flags+=(-fsanitize=thread) fi diff --git a/test/audio-quality/clients/js/vite.config.ts b/test/audio-quality/clients/js/vite.config.ts index c78b399fe90..88cc159a7c4 100644 --- a/test/audio-quality/clients/js/vite.config.ts +++ b/test/audio-quality/clients/js/vite.config.ts @@ -8,7 +8,7 @@ * @module */ import { defineConfig } from "vite"; -import { worklet } from "../../../../js/common/vite-plugin-worklet"; +import { worklet } from "../../../../js/common/vite-plugin-worklet.ts"; /** * esnext keeps WebCodecs / WebTransport syntax intact for headless Chromium. Three pages: the player diff --git a/test/interop/clients/js/vite.config.ts b/test/interop/clients/js/vite.config.ts index e4389d9ffee..a3c19d7c550 100644 --- a/test/interop/clients/js/vite.config.ts +++ b/test/interop/clients/js/vite.config.ts @@ -7,7 +7,7 @@ * @module */ import { defineConfig } from "vite"; -import { worklet } from "../../../../js/common/vite-plugin-worklet"; +import { worklet } from "../../../../js/common/vite-plugin-worklet.ts"; /** esnext keeps WebCodecs / WebTransport syntax intact for headless Chromium. */ export default defineConfig({ From a1c790817bfaca207e17c781c3b14625af44264d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 19:41:32 -0700 Subject: [PATCH 3/4] style(py): wrap the qualified json.py signature Co-Authored-By: Claude Opus 5.5 --- py/moq-rs/moq/json.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/py/moq-rs/moq/json.py b/py/moq-rs/moq/json.py index f0734e32cd4..5179910e0de 100644 --- a/py/moq-rs/moq/json.py +++ b/py/moq-rs/moq/json.py @@ -97,7 +97,9 @@ class StreamProducer: carries is refused. """ - def __init__(self, broadcast: BroadcastProducer, track: publish.TrackProducer, *, compression: bool = False) -> None: + def __init__( + self, broadcast: BroadcastProducer, track: publish.TrackProducer, *, compression: bool = False + ) -> None: config = MoqJsonStreamConfig(compression=compression) self._inner = MoqJsonStreamProducer(broadcast._inner, track._inner, config) From bf2803c27add7c8feab932a529f3465917e5dda6 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 21 Oct 2026 10:28:24 -0700 Subject: [PATCH 4/4] docs(qmux): document the public surface the build now checks @moq/qmux landed alongside the doc-comment check in the package build, so its build failed on the undocumented WebTransport members. Co-Authored-By: Claude Opus 5.5 --- js/qmux/src/error.ts | 4 ++++ js/qmux/src/session.ts | 12 +++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/js/qmux/src/error.ts b/js/qmux/src/error.ts index 56b33ae87c1..90e070eebf3 100644 --- a/js/qmux/src/error.ts +++ b/js/qmux/src/error.ts @@ -10,7 +10,9 @@ * socket error, decode failure, ...). */ export class SessionError extends Error implements Pick { + /** Always `"session"`, marking a session-level failure. */ readonly source = "session" as const; + /** Always `null`, since no stream was reset. */ readonly streamErrorCode = null; constructor(message: string, options?: { cause?: unknown }) { @@ -28,7 +30,9 @@ export class SessionError extends Error implements Pick { + /** Always `"stream"`, marking a stream reset. */ readonly source = "stream" as const; + /** The application error code the reset carries. */ readonly streamErrorCode: number; constructor(code: number, message = "stream reset") { diff --git a/js/qmux/src/session.ts b/js/qmux/src/session.ts index 9f0de0ee1f0..95ebebddcbf 100644 --- a/js/qmux/src/session.ts +++ b/js/qmux/src/session.ts @@ -500,6 +500,7 @@ interface StreamFlowState { recvConsumed: bigint; } +/** A `WebTransport` session carried over QMux on a WebSocket. */ export default class Session implements WebTransport { // The transport: a native `WebSocketStream` when the platform has one (real // backpressure), otherwise the `WebSocketStream` ponyfill over a plain @@ -531,27 +532,32 @@ export default class Session implements WebTransport { // with the actual version derived from `ws.protocol`. #version: WireFormat = "webtransport"; + #protocol = ""; /** The negotiated application-level subprotocol, or empty string if none. * * The prefix is stripped; this returns only the application protocol name. */ - #protocol = ""; get protocol(): string { return this.#protocol; } + /** Resolves once the handshake completes, or rejects if it fails. */ readonly ready: Promise; #readyResolve: () => void; #readyReject: (err: Error) => void; + /** Resolves on a graceful close, or rejects on a transport error. */ readonly closed: Promise; #closedResolve: (info: WebTransportCloseInfo) => void; #closedReject: (err: Error) => void; + /** Bidirectional streams the peer opens. */ readonly incomingBidirectionalStreams: ReadableStream; #incomingBidirectionalQueue: IncomingStreamQueue; + /** Unidirectional streams the peer opens. */ readonly incomingUnidirectionalStreams: ReadableStream>; #incomingUnidirectionalQueue: IncomingStreamQueue>; + /** Datagrams sent as QMux DATAGRAM frames, dropped when unsupported or oversized. */ readonly datagrams = new Datagrams((data) => this.#sendDatagram(data)); // Flow control state @@ -1755,6 +1761,7 @@ export default class Session implements WebTransport { }); } + /** Open a bidirectional stream once the session is ready. */ async createBidirectionalStream(options?: WebTransportSendStreamOptions): Promise { await this.ready; @@ -1834,6 +1841,7 @@ export default class Session implements WebTransport { return { readable: recvStream.readable, writable: writer }; } + /** Open a unidirectional stream once the session is ready. */ async createUnidirectionalStream(options?: WebTransportSendStreamOptions): Promise> { await this.ready; @@ -2012,6 +2020,7 @@ export default class Session implements WebTransport { void sleep(Nano.fromMilli(Milli(100))).then(() => this.#transportClose()); } + /** Close the session, sending `closeCode` and `reason` to the peer. */ close(info?: { closeCode?: number; reason?: string }) { if (this.#closed) return; const code = info?.closeCode ?? 0; @@ -2033,6 +2042,7 @@ export default class Session implements WebTransport { this.#wss?.setHighWaterMark?.(this.#sendBufferSize); } + /** Always `"default"`, since TCP owns congestion control. */ get congestionControl(): string { return "default"; }