diff --git a/.github/justfile b/.github/justfile index 7479ab77ce2..73d61a8e986 100644 --- a/.github/justfile +++ b/.github/justfile @@ -47,6 +47,7 @@ test: python3 sh/gh/gst-formula.test.py python3 sh/changed.test.py python3 sh/large-files.test.py + python3 sh/text.test.py python3 sh/dispatch-query.test.py python3 sh/dispatch-query.test.py --bench python3 sh/gh/test-inventory.test.py diff --git a/.github/workflows/pullfrog.yml b/.github/workflows/pullfrog.yml index e7e3305cf48..f698e046c9b 100644 --- a/.github/workflows/pullfrog.yml +++ b/.github/workflows/pullfrog.yml @@ -1,4 +1,4 @@ -# PULLFROG ACTION — DO NOT EDIT EXCEPT WHERE INDICATED +# PULLFROG ACTION - DO NOT EDIT EXCEPT WHERE INDICATED name: Pullfrog run-name: ${{ inputs.name || github.workflow }} on: @@ -58,15 +58,15 @@ jobs: # AZURE_RESOURCE_NAME: my-resource # AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }} # AZURE_DEPLOYMENT: - # both limits are required — set them to the real limits of that model + # both limits are required - set them to the real limits of that model # AZURE_CONTEXT: "400000" # AZURE_MAX_OUTPUT: "128000" - # for any OpenAI-compatible endpoint — LiteLLM, Cloudflare AI Gateway, + # for any OpenAI-compatible endpoint - LiteLLM, Cloudflare AI Gateway, # self-hosted vLLM (https://docs.pullfrog.com/openai-compatible) # OPENAI_COMPATIBLE_BASE_URL: https://litellm.example.com/v1 # OPENAI_COMPATIBLE_API_KEY: ${{ secrets.OPENAI_COMPATIBLE_API_KEY }} # OPENAI_COMPATIBLE_MODEL: - # both limits are required — set them to the real limits of that model + # both limits are required - set them to the real limits of that model # OPENAI_COMPATIBLE_CONTEXT: "128000" # OPENAI_COMPATIBLE_MAX_OUTPUT: "16384" diff --git a/.github/workflows/release-go.yml b/.github/workflows/release-go.yml index 64bb72e149f..88341e9797e 100644 --- a/.github/workflows/release-go.yml +++ b/.github/workflows/release-go.yml @@ -8,7 +8,7 @@ name: Release Go # (registry is the gate, like release-plz / the PyPI check in release.sh). # # Triggers: -# * push to release — a wrapper API or VERSION change. The ffi require is +# * push to release - a wrapper API or VERSION change. The ffi require is # pinned to the current crate version, whose mirror tag # already exists in steady state. If it's missing (a # release is mid-flight) or rs/moq-ffi changed since diff --git a/demo/boy/src/worker.ts b/demo/boy/src/worker.ts index 3f7db4ec4b3..a2a9202380c 100644 --- a/demo/boy/src/worker.ts +++ b/demo/boy/src/worker.ts @@ -1,4 +1,4 @@ -// Cloudflare Worker for rom.moq.dev — serves Game Boy ROMs from R2. +// Cloudflare Worker for rom.moq.dev - serves Game Boy ROMs from R2. interface Env { ROM: R2Bucket; diff --git a/demo/web/src/viz.ts b/demo/web/src/viz.ts index aa50c0fa0f7..f43f613534e 100644 --- a/demo/web/src/viz.ts +++ b/demo/web/src/viz.ts @@ -69,7 +69,7 @@ export function graph(parent: Signals.Effect, title: string, opts?: GraphOptions const value = document.createElement("span"); value.className = "font-mono"; value.style.color = color; - value.textContent = "—"; + value.textContent = "-"; header.append(label, value); const canvas = document.createElement("canvas"); @@ -134,7 +134,7 @@ export function graph(parent: Signals.Effect, title: string, opts?: GraphOptions const push = (v: number | undefined) => { samples.push(v !== undefined && Number.isFinite(v) ? Math.max(0, v) : 0); while (samples.length > capacity) samples.shift(); - value.textContent = v !== undefined && Number.isFinite(v) ? (opts?.format?.(v) ?? v.toFixed(0)) : "—"; + value.textContent = v !== undefined && Number.isFinite(v) ? (opts?.format?.(v) ?? v.toFixed(0)) : "-"; draw(); }; diff --git a/drafts/draft-lcurley-moq-lite.md b/drafts/draft-lcurley-moq-lite.md index 51e9aeb675c..c1440ba7a00 100644 --- a/drafts/draft-lcurley-moq-lite.md +++ b/drafts/draft-lcurley-moq-lite.md @@ -524,7 +524,7 @@ A subscriber opens a Fetch Stream (0x3) to request a single Group from a Track. The subscriber sends a FETCH message containing the broadcast path, track name, priority, group sequence, and the frame range within that group. Unlike SUBSCRIBE, FETCH works on both live and ended broadcasts; it is the only way to read an ended one. -The publisher responds with FRAME messages directly on the same bidirectional stream — there is no response header. +The publisher responds with FRAME messages directly on the same bidirectional stream - there is no response header. The Group Sequence and index of the first returned frame are implicit, taken from the original FETCH request. Because there is no response header, a publisher that cannot serve the requested frame range in full MUST reset the stream rather than return a shorter run; the subscriber has no way to learn where a truncated response actually started. As with a subscription, the subscriber MUST already have the track's [TRACK_INFO](#track-info) to parse the returned frames; because the properties are immutable, a single Track Stream lookup is reused across every FETCH of that track (group-by-group fetches do not re-fetch it). @@ -1619,10 +1619,10 @@ The last frame to return (inclusive), encoded as the absolute frame index + 1. A value of 0 means through the end of the group (default). A `Frame End` below `Frame Start` once decoded is a protocol violation; equal bounds are a legal single-frame range. -The publisher responds with FRAME messages directly on the same stream — there is no response header. +The publisher responds with FRAME messages directly on the same stream - there is no response header. The subscriber parses them using the track's [TRACK_INFO](#track-info), which it MUST already have (see the [Track Stream](#track-stream)); the group sequence and the index of the first frame are implicit from the FETCH request. The publisher FINs the stream after the last frame, or resets on error. -There is no FETCH_ERROR message — the publisher signals failure by resetting the stream. +There is no FETCH_ERROR message - the publisher signals failure by resetting the stream. A publisher holding fewer frames than requested MUST reset rather than truncate, since a short response is indistinguishable from one that started elsewhere. A group that ends before `Frame End` is not a truncation: the publisher FINs after the last frame it has, provided the group is complete and it served everything from `Frame Start` onward. diff --git a/drafts/draft-lcurley-moq-probe.md b/drafts/draft-lcurley-moq-probe.md index bd1f6d559cf..a50a3467a70 100644 --- a/drafts/draft-lcurley-moq-probe.md +++ b/drafts/draft-lcurley-moq-probe.md @@ -49,7 +49,7 @@ This means the available bandwidth is often underestimated, and the subscriber h This is particularly problematic for adaptive bitrate (ABR) algorithms. A viewer may get stuck at a low quality rendition indefinitely because the congestion window never grows to reflect the true link capacity. -If the viewer does attempt to switch to a higher rendition without first probing, they risk buffering — either because the congestion window has not been warmed up to support the higher bitrate, or because the network genuinely cannot sustain it. +If the viewer does attempt to switch to a higher rendition without first probing, they risk buffering - either because the congestion window has not been warmed up to support the higher bitrate, or because the network genuinely cannot sustain it. Without probing, the subscriber cannot distinguish between these two cases. {{moqt}} Section 3.7.2 suggests subscribing to additional tracks at low priority or sending padding to fill the congestion window during probing intervals. diff --git a/js/hang/README.md b/js/hang/README.md index 84cf721f651..875b535c7af 100644 --- a/js/hang/README.md +++ b/js/hang/README.md @@ -11,9 +11,9 @@ Core media library for [Media over QUIC](https://moq.dev/) (MoQ). Provides share ## Features -- **Catalog** — JSON track describing other tracks and their codec properties (audio, video, chat, location, etc.) -- **Container** — Media framing in two formats: CMAF (fMP4) and Legacy (varint-timestamp + raw codec bitstream) -- **Utilities** — Hex encoding, Opus and AAC audio polyfill ([`@moq/libav`](../libav)), latency computation, browser detection workarounds +- **Catalog** - JSON track describing other tracks and their codec properties (audio, video, chat, location, etc.) +- **Container** - Media framing in two formats: CMAF (fMP4) and Legacy (varint-timestamp + raw codec bitstream) +- **Utilities** - Hex encoding, Opus and AAC audio polyfill ([`@moq/libav`](../libav)), latency computation, browser detection workarounds Browser support detection is provided by [``](../watch) and [``](../publish). @@ -32,10 +32,10 @@ bun add @moq/hang ```typescript import * as Hang from "@moq/hang"; -// Catalog — describes tracks and their codec properties +// Catalog - describes tracks and their codec properties import * as Catalog from "@moq/hang/catalog"; -// Container — Legacy and LOC framing +// Container - Legacy and LOC framing import * as Container from "@moq/hang/container"; // CMAF decoding is a separate synchronous module, loaded only when needed. @@ -53,10 +53,10 @@ import * as Publish from "@moq/publish"; ## Related Packages -- **[@moq/watch](../watch)** — Subscribe to and render MoQ broadcasts -- **[@moq/publish](../publish)** — Publish media to MoQ broadcasts -- **[@moq/net](../lite)** — Core pub/sub transport protocol -- **[@moq/signals](../signals)** — Reactive signals library +- **[@moq/watch](../watch)** - Subscribe to and render MoQ broadcasts +- **[@moq/publish](../publish)** - Publish media to MoQ broadcasts +- **[@moq/net](../lite)** - Core pub/sub transport protocol +- **[@moq/signals](../signals)** - Reactive signals library ## License diff --git a/js/hang/src/cmaf/decode.ts b/js/hang/src/cmaf/decode.ts index 8588cab8352..409b0c2ccec 100644 --- a/js/hang/src/cmaf/decode.ts +++ b/js/hang/src/cmaf/decode.ts @@ -288,7 +288,7 @@ function extractAudioSpecificConfig(esds: Uint8Array): Uint8Array | undefined { } if (tag === 0x05) { - // Found DecoderSpecificInfo — payload is the AudioSpecificConfig + // Found DecoderSpecificInfo - payload is the AudioSpecificConfig if (offset + size <= esds.length) { return new Uint8Array(esds.buffer, esds.byteOffset + offset, size); } @@ -302,7 +302,7 @@ function extractAudioSpecificConfig(esds: Uint8Array): Uint8Array | undefined { } else if (tag === 0x04) { offset += 13; // objectTypeIndication (1) + streamType (1) + bufferSizeDB (3) + maxBitrate (4) + avgBitrate (4) } else { - // Unknown tag — skip its payload entirely + // Unknown tag - skip its payload entirely offset += size; } } diff --git a/js/moq-boy/README.md b/js/moq-boy/README.md index 91d40623939..07de484e06e 100644 --- a/js/moq-boy/README.md +++ b/js/moq-boy/README.md @@ -19,7 +19,7 @@ npm add @moq/boy ## Web Components -### `` — Game Grid +### `` - Game Grid Shows a grid of all active Game Boy sessions. Click a game to expand and play. diff --git a/js/moq-boy/src/element.tsx b/js/moq-boy/src/element.tsx index 5e12f823236..c66f79d79ca 100644 --- a/js/moq-boy/src/element.tsx +++ b/js/moq-boy/src/element.tsx @@ -21,14 +21,14 @@ const DEFAULT_PREFIX = Moq.Path.from("boy"); const cleanup = new FinalizationRegistry((signals) => signals.close()); /** - * `` web component — discovers and manages Game Boy streaming sessions, + * `` web component - discovers and manages Game Boy streaming sessions, * and renders the interactive UI in a Shadow DOM. * * Attributes: - * - `url` — MoQ relay URL - * - `prefix` — Base path prefix (default: "boy"). Derives prefix-game and prefix-viewer. - * - `prefix-game` — Path prefix for game broadcasts (default: `{prefix}/game`) - * - `prefix-viewer` — Path prefix for viewer broadcasts (default: `{prefix}/viewer`) + * - `url` - MoQ relay URL + * - `prefix` - Base path prefix (default: "boy"). Derives prefix-game and prefix-viewer. + * - `prefix-game` - Path prefix for game broadcasts (default: `{prefix}/game`) + * - `prefix-viewer` - Path prefix for viewer broadcasts (default: `{prefix}/viewer`) */ export default class MoqBoy extends HTMLElement { /** The attributes the browser reports changes to. */ diff --git a/js/moq-boy/src/ui/components/GameCard.tsx b/js/moq-boy/src/ui/components/GameCard.tsx index bb06ff28d4b..8773fa1c381 100644 --- a/js/moq-boy/src/ui/components/GameCard.tsx +++ b/js/moq-boy/src/ui/components/GameCard.tsx @@ -27,7 +27,7 @@ function GameCardInner() { game.canvas.set(canvasRef); }); - // Keyboard input — preventDefault when expanded or hovered. + // Keyboard input - preventDefault when expanded or hovered. const isActive = () => game.expanded.peek() === game.sessionId || game.hovered.peek(); const onKeyDown = (e: KeyboardEvent) => { diff --git a/js/publish/README.md b/js/publish/README.md index c3655e7219a..963b89647d2 100644 --- a/js/publish/README.md +++ b/js/publish/README.md @@ -66,14 +66,14 @@ The simplest way to publish a stream: | `url` | string | required | Relay server URL | | `name` | string | required | Broadcast name | | `versions` | string | stable versions | Comma-separated protocol versions to offer, most preferred first, like `moq-transport-18,moq-lite-06` | -| `source` | string | — | `"camera"`, `"screen"`, `"file"` | +| `source` | string | - | `"camera"`, `"screen"`, `"file"` | | `muted` | boolean | false | Stop publishing audio; the microphone stays open | | `invisible` | boolean | false | Stop publishing video; the camera stays open | | `preview` | string | `"source"` | What the preview renders: `"source"`, `"encoded"`, `"none"` | | `backend` | string | `"auto"` | Graphics API for a `` preview: `"auto"`, `"webgpu"`, `"2d"` | | `announce` | string | `"source"` | When to advertise: `"always"` or `"source"` (once media is captured). A camera source waits for every enabled track, and `"source"` waits until each captured track's config resolves or fails, so the first catalog lists every rendition. After its first announcement, a camera stays announced until deselected or disconnected, including mute, hide, device swaps, and capture retries. The broadcast is created while connected either way. To keep it out of discovery, use a hidden name with a `.`-prefixed segment; it is still subscribable by name. | | `stats` | boolean | false | Publish a `stats.json` track, named in the catalog, reporting what each rendition sent and the PROBE round-trip time | -| `echo-path` | string | — | Solicit viewer feedback under this path, relative to the broadcast (from `room/live`, `viewers` is `room/viewers`) | +| `echo-path` | string | - | Solicit viewer feedback under this path, relative to the broadcast (from `room/live`, `viewers` is `room/viewers`) | Property and built-in UI changes reflect into attributes. Invalid URLs, version lists, and source/preview/backend/announce modes are refused: their property setters throw, and malformed markup diff --git a/js/publish/src/ui/components/stats-tab.ts b/js/publish/src/ui/components/stats-tab.ts index 44872a228a0..fb7506e1798 100644 --- a/js/publish/src/ui/components/stats-tab.ts +++ b/js/publish/src/ui/components/stats-tab.ts @@ -55,18 +55,18 @@ export function statsTab(parent: Effect, publish: MoqPublish): HTMLElement { const display = capture ? effect.get(capture.out.display) : undefined; const cfg = effect.get(publish.video.out.catalog); videoCard.el.style.display = display ? "" : "none"; - vRes.textContent = display ? `${display.width}×${display.height}` : "—"; - vCodec.textContent = cfg?.codec ?? "—"; + vRes.textContent = display ? `${display.width}×${display.height}` : "-"; + vCodec.textContent = cfg?.codec ?? "-"; }); parent.run((effect) => { const cfg = effect.get(publish.audio.out.catalog); audioCard.el.style.display = cfg ? "" : "none"; if (!cfg) return; - aCodec.textContent = cfg.codec ?? "—"; - aRate.textContent = cfg.sampleRate ? formatHz(cfg.sampleRate) : "—"; - aChannels.textContent = cfg.numberOfChannels ? `${cfg.numberOfChannels}` : "—"; - aBitrate.textContent = cfg.bitrate ? formatBitrate(cfg.bitrate) : "—"; + aCodec.textContent = cfg.codec ?? "-"; + aRate.textContent = cfg.sampleRate ? formatHz(cfg.sampleRate) : "-"; + aChannels.textContent = cfg.numberOfChannels ? `${cfg.numberOfChannels}` : "-"; + aBitrate.textContent = cfg.bitrate ? formatBitrate(cfg.bitrate) : "-"; }); parent.run((effect) => { @@ -74,8 +74,8 @@ export function statsTab(parent: Effect, publish: MoqPublish): HTMLElement { const status = effect.get(publish.connection.status); const name = effect.get(publish.broadcast.in.name); nStatus.textContent = status; - nServer.textContent = url?.host ?? "—"; - nName.textContent = name?.toString() || "—"; + nServer.textContent = url?.host ?? "-"; + nName.textContent = name?.toString() || "-"; }); // Live graphs: frame rate from captured frames, bitrate measured from the bytes we encoded. @@ -123,7 +123,7 @@ export function statsTab(parent: Effect, publish: MoqPublish): HTMLElement { vFpsGraph.push(elapsed > 0 && frameDelta > 0 ? frameDelta / (elapsed / 1000) : undefined); const stats = publish.video.out.stats.peek(); - vTarget.textContent = stats.bitrate !== undefined ? formatBitrate(stats.bitrate) : "—"; + vTarget.textContent = stats.bitrate !== undefined ? formatBitrate(stats.bitrate) : "-"; vSkipped.textContent = `${stats.skipped}`; const bytes = encoded(); diff --git a/js/qmux/README.md b/js/qmux/README.md index 29a26e8dc0f..7259c24db39 100644 --- a/js/qmux/README.md +++ b/js/qmux/README.md @@ -41,7 +41,7 @@ throws `NotSupportedError`. ### Servers `Session.accept` runs the other role, over a WebSocket your server has already accepted. The host -performs the upgrade, so it — not this library — chooses the subprotocol; `selectSubprotocol` picks +performs the upgrade, so it - not this library - chooses the subprotocol; `selectSubprotocol` picks the value to accept from what the client offered, and the session reads the wire-format version back off the socket. @@ -62,7 +62,7 @@ Deno.serve((req) => { }) ``` -Any already-accepted `WebSocket` works — `Deno.upgradeWebSocket`, Node's `ws`, or a +Any already-accepted `WebSocket` works - `Deno.upgradeWebSocket`, Node's `ws`, or a `WebSocketStream`. The session refuses missing or unsupported subprotocols. Reject the upgrade when `selectSubprotocol` returns `undefined`. If your host doesn't expose the negotiated value on the socket, pass it explicitly as `{ protocol }`. @@ -76,8 +76,8 @@ the stream-id space each side owns. - **Fulfills** with `{ closeCode, reason }` on a graceful end: either side called `close()`, which puts an `APPLICATION_CLOSE` (`0x1d`) on the wire. -- **Rejects** on an abnormal end: a `CONNECTION_CLOSE` (`0x1c`) arrived — the transport variant a - peer sends when *it* caught a protocol violation — or no close frame arrived at all, because the +- **Rejects** on an abnormal end: a `CONNECTION_CLOSE` (`0x1c`) arrived - the transport variant a + peer sends when *it* caught a protocol violation - or no close frame arrived at all, because the socket dropped or the peer went idle. This endpoint rejects the same way when it catches the peer violating the protocol, and sends a `CONNECTION_CLOSE` so the peer rejects too. @@ -91,7 +91,7 @@ try { } ``` -Don't reach for `closeCode` to tell the two apart — close codes are application-defined, so a +Don't reach for `closeCode` to tell the two apart - close codes are application-defined, so a graceful close may carry any value, including one that looks like a violation, and an app closing with `1006` is indistinguishable from a dropped socket. Which frame arrived is the signal, and the settled state is how you read it. diff --git a/js/qmux/src/credit.ts b/js/qmux/src/credit.ts index e32653639c9..abbc07f43b1 100644 --- a/js/qmux/src/credit.ts +++ b/js/qmux/src/credit.ts @@ -2,7 +2,7 @@ * * Replenishes only when the remaining advertised window has dropped to at most * half, and always sets the new limit relative to bytes the application has - * actually **consumed** (`consumed` is cumulative) — never relative to bytes + * actually **consumed** (`consumed` is cumulative) - never relative to bytes * received. That keeps the peer's send credit bounded to one `window` ahead of * the reader: a stalled reader stops advancing `consumed`, so the limit stops * moving and the peer's credit drains. diff --git a/js/qmux/src/error.ts b/js/qmux/src/error.ts index d8aaa95d5fa..a5769709acd 100644 --- a/js/qmux/src/error.ts +++ b/js/qmux/src/error.ts @@ -2,8 +2,8 @@ * WebTransport spec requires `WebTransport.closed` to reject with. * * The native `WebTransportError` constructor only exists where the browser - * already implements WebTransport — which is precisely where this polyfill is - * not used — so mint our own rather than feature-detect a global that will not + * already implements WebTransport - which is precisely where this polyfill is + * not used - so mint our own rather than feature-detect a global that will not * be there. `name`, `source`, and `streamErrorCode` match the interface, so a * consumer branching on `err.source === "session"` works against the native API * and this polyfill alike; `cause` carries the underlying failure (idle timeout, diff --git a/js/qmux/src/frame.test.ts b/js/qmux/src/frame.test.ts index 2f5e06c69d1..bd05fa7496e 100644 --- a/js/qmux/src/frame.test.ts +++ b/js/qmux/src/frame.test.ts @@ -48,7 +48,7 @@ describe("QMux01 record framing", () => { // parameter must never appear and decoding must throw. const bytes = (...vals: number[]) => new Uint8Array(vals); const record = bytes( - // frame type 0x3f5153300d0a0d0a — 8-byte varint, so the high tag bits + // frame type 0x3f5153300d0a0d0a - 8-byte varint, so the high tag bits // make the first wire byte 0xff (the "\xffQS0\r\n\r\n" magic). 0xff, 0x51, @@ -60,7 +60,7 @@ describe("QMux01 record framing", () => { 0x0a, // length = 9 (param id 8 bytes + len 1 byte) 0x09, - // param id 0x3d4f9c2a8b1e6075 — 8-byte varint, first wire byte 0xfd. + // param id 0x3d4f9c2a8b1e6075 - 8-byte varint, first wire byte 0xfd. 0xfd, 0x4f, 0x9c, @@ -150,7 +150,7 @@ describe("QMux01 record framing", () => { initialMaxStreamsBidi: 0n, initialMaxStreamsUni: 0n, maxDatagramFrameSize: 0n, - // Deliberately set to 0 — exercises the encoder's "skip-if-zero" + decoder's default seeding. + // Deliberately set to 0 - exercises the encoder's "skip-if-zero" + decoder's default seeding. maxRecordSize: 0n, resetStreamAt: false, }, @@ -195,7 +195,7 @@ describe("QMux02 (draft-02)", () => { }); test("a duplicate transport parameter is rejected", () => { - // QX_TRANSPORT_PARAMETERS carrying initial_max_data (0x04) twice — a + // QX_TRANSPORT_PARAMETERS carrying initial_max_data (0x04) twice - a // protocol error, matching the Rust decoder's DuplicateParam. const bytes = new Uint8Array([ 0xff, @@ -357,7 +357,7 @@ describe("maxStreamPayload", () => { test("fills the budget at varint boundaries", () => { // Where the length field changes width, subtracting the width of the space // left (rather than of the payload) leaves bytes unused: at 16387 that is - // 16380, but 16382 fits — its own length still encodes in two bytes. + // 16380, but 16382 fits - its own length still encodes in two bytes. for (let budget = 60n; budget <= 70n; budget++) assertTight(budget, id, 0n); for (let budget = 16380n; budget <= 16392n; budget++) assertTight(budget, id, 0n); expect(Frame.maxStreamPayload("qmux-01", 16387n, id, 0n)).toBe(16382n); @@ -372,7 +372,7 @@ describe("maxStreamPayload", () => { }); test("handles the largest advertised max_record_size", () => { - // max_record_size is a varint, so a peer may advertise 2^62-1 — a value + // max_record_size is a varint, so a peer may advertise 2^62-1 - a value // `number` cannot hold exactly. assertTight(VarInt.MAX, id, 0n); }); diff --git a/js/qmux/src/frame.ts b/js/qmux/src/frame.ts index a2dcbcaf5e8..0b17f9052a4 100644 --- a/js/qmux/src/frame.ts +++ b/js/qmux/src/frame.ts @@ -26,7 +26,7 @@ const VARINT_MAX = [63n, 16383n, 1073741823n, 4611686018427387903n]; * * The length varint's width depends on the payload it describes, so this is a * fixed point rather than a subtraction: at `available = 16384`, reserving four - * bytes (the width of 16384 itself) yields 16380, but 16382 fits — its length + * bytes (the width of 16384 itself) yields 16380, but 16382 fits - its length * still encodes in two. Solve it by asking, for each width, what the largest * payload that width can describe *and* leave room for is, then take the best. */ function maxLengthPrefixedPayload(available: bigint): bigint { @@ -45,8 +45,8 @@ function maxLengthPrefixedPayload(available: bigint): bigint { /** Largest STREAM payload that keeps the encoded frame within `budget` bytes. * - * The budget is what the peer accepts for one frame — its `max_record_size` on - * the record-framed drafts, {@link MAX_FRAME_SIZE} otherwise — so the frame's own + * The budget is what the peer accepts for one frame - its `max_record_size` on + * the record-framed drafts, {@link MAX_FRAME_SIZE} otherwise - so the frame's own * header comes out of it: the type, the stream ID, the offset and * the length varint. Header widths are the ones this frame actually encodes, not * a worst-case reservation. The session applies any send-only compatibility @@ -106,7 +106,7 @@ export interface ConnectionClose { * application code/reason. The receiver *fulfills* `closed`. Per RFC 9000 §19.19 * the application variant omits the Frame Type field. * - * The close subtype, not the code, carries the fulfill/reject distinction — + * The close subtype, not the code, carries the fulfill/reject distinction - * codes are application-defined, so a graceful close may use any value. */ export interface ApplicationClose { type: "application_close"; @@ -259,7 +259,7 @@ export function encode(frame: Any, _version: WireFormat): Uint8Array { /** Returns true if the version uses the QMux Record framing layer and the * features introduced alongside it (QX_PING keep-alive, PADDING, datagrams, - * max_record_size) — draft-01 and later. */ + * max_record_size) - draft-01 and later. */ export function usesRecords(version: WireFormat): boolean { return version === "qmux-01" || version === "qmux-02"; } @@ -468,7 +468,7 @@ function encodeQMux(frame: Any): Uint8Array { case "datagram": { // Length-prefixed form (0x31). Datagrams are only sent on QMux01, where // the record (WS message) boundary would already delimit a lone 0x30 - // datagram — but we emit 0x31 so it stays self-delimiting even if a + // datagram - but we emit 0x31 so it stays self-delimiting even if a // record ever carries a frame after it. The length costs 1-2 bytes. const lengthVi = VarInt.from(frame.data.length); let buffer = new Uint8Array(new ArrayBuffer(8 + 8 + frame.data.length), 0, 0); @@ -838,7 +838,7 @@ function decodeQMuxOne(buffer: Uint8Array): [Any | null, Uint8Array] | null { return [{ type: "ping_response", sequence: v.value }, buffer]; } - // DATAGRAM without length — payload runs to the end of the record + // DATAGRAM without length - payload runs to the end of the record if (frameType === 0x30n) { const [data, rest] = take(buffer, buffer.byteLength); return [{ type: "datagram", data: own(data), lengthPrefixed: false }, rest]; diff --git a/js/qmux/src/index.ts b/js/qmux/src/index.ts index c1c37522f97..996fa3580e0 100644 --- a/js/qmux/src/index.ts +++ b/js/qmux/src/index.ts @@ -35,7 +35,7 @@ export function install(options?: InstallOptions): boolean { if ("WebTransport" in globalThis) return false; const versions = options?.versions; const requireProtocol = options?.requireProtocol; - // biome-ignore lint/suspicious/noExplicitAny: polyfill — extending Session to match the WebTransport constructor signature + // biome-ignore lint/suspicious/noExplicitAny: polyfill - extending Session to match the WebTransport constructor signature (globalThis as any).WebTransport = class extends Session { constructor(url: string | URL, options?: WebTransportOptions) { super(url, { ...options, versions, requireProtocol }); diff --git a/js/qmux/src/recv.ts b/js/qmux/src/recv.ts index 18443b11f97..00da7f49f45 100644 --- a/js/qmux/src/recv.ts +++ b/js/qmux/src/recv.ts @@ -2,7 +2,7 @@ * * Buffers incoming STREAM data and hands it to the application's * `ReadableStream` *on demand* (pull-driven). `onConsume` fires only when bytes - * are actually delivered to the reader — never on mere receipt — so flow-control + * are actually delivered to the reader - never on mere receipt - so flow-control * credit (MAX_STREAM_DATA) tracks the application's read rate. Combined with the * sender claiming credit before sending, this bounds how far the peer can run * ahead of a slow reader: it can buffer at most one receive window of undelivered @@ -59,7 +59,7 @@ export class RecvStream { const chunk = this.#queue.shift() as Uint8Array; this.#delivered += BigInt(chunk.byteLength); controller.enqueue(chunk); - // Credit must track *delivery*, not receipt — do NOT move onConsume + // Credit must track *delivery*, not receipt - do NOT move onConsume // into push(). With a zero high-water mark, pull only runs for an // application read, so even the first chunk cannot be prefetched into // an unaccepted stream's hidden ReadableStream queue. diff --git a/js/qmux/src/scheduler.test.ts b/js/qmux/src/scheduler.test.ts index d1510e584d8..e3013341456 100644 --- a/js/qmux/src/scheduler.test.ts +++ b/js/qmux/src/scheduler.test.ts @@ -63,7 +63,7 @@ async function tick(n = 1): Promise { } } -/** Wait until `fn()` is true, throwing if it never becomes true within budget — +/** Wait until `fn()` is true, throwing if it never becomes true within budget - * so a scheduler regression fails the test instead of silently proceeding. */ async function until(fn: () => boolean, tries = 200): Promise { for (let i = 0; i < tries && !fn(); i++) { @@ -177,7 +177,7 @@ describe("SendScheduler", () => { const sched = new SendScheduler(sink); // Give the transport room first, so the datagram is admitted (a datagram - // enqueued under backpressure would be dropped — see the next test). + // enqueued under backpressure would be dropped - see the next test). sink.release(3); sched.setSendOrder(1n, 0); void sched.enqueueStream(1n, frame(10)); diff --git a/js/qmux/src/scheduler.ts b/js/qmux/src/scheduler.ts index a7098934a2c..e83873c61db 100644 --- a/js/qmux/src/scheduler.ts +++ b/js/qmux/src/scheduler.ts @@ -3,7 +3,7 @@ * A single writer loop owns the socket. Frames are written in priority order: * control frames (flow control, resets, pings, close) always preempt stream * data, and among stream data the highest `sendOrder` wins (W3C WebTransport - * convention — larger value is sent first), with round-robin among equal + * convention - larger value is sent first), with round-robin among equal * priorities. Prioritization only bites under backpressure; when the sink has * room every frame is written as soon as it's queued. * @@ -18,7 +18,7 @@ export interface SendSink { * high-water mark. Rejects if the socket is closed/errored. */ ready(): Promise; /** Write bytes to the socket, resolving once the write is accepted. Rejects - * if the write fails — the scheduler must observe this rather than resolve + * if the write fails - the scheduler must observe this rather than resolve * the frame's promise as if it succeeded. */ write(bytes: Uint8Array): Promise; /** Synchronous backpressure probe: true when the sink can take a write now @@ -29,7 +29,7 @@ export interface SendSink { } /** Backpressure-aware sink over a `WebSocketStream` writable. The writable's - * `writer.ready` is the backpressure signal — native `WebSocketStream` ties it + * `writer.ready` is the backpressure signal - native `WebSocketStream` ties it * to the real send buffer, while the `WebSocketStream` ponyfill drives * it from `bufferedAmount` against a (resizable) high-water mark. */ export class WritableStreamSink implements SendSink { @@ -103,7 +103,7 @@ export class SendScheduler { #latest = new Map(); // Datagrams: serviced after control, ahead of stream data. Bounded and lossy - // (unlike #control) since unreliable datagrams are meant to be droppable — + // (unlike #control) since unreliable datagrams are meant to be droppable - // shed on transport backpressure or a full lane. #datagrams: Uint8Array[] = []; @@ -124,7 +124,7 @@ export class SendScheduler { this.#sink = sink; this.#onActivity = options?.onActivity ?? (() => {}); this.#onFail = options?.onFail ?? (() => {}); - // Start the writer loop. It never rejects — failures are captured by #fail. + // Start the writer loop. It never rejects - failures are captured by #fail. void this.#run(options?.first); } @@ -143,10 +143,10 @@ export class SendScheduler { this.#signal(); } - /** Queue a datagram frame — best-effort, serviced after control but ahead of + /** Queue a datagram frame - best-effort, serviced after control but ahead of * stream data. Dropped (rather than queued) when: * - the scheduler is closed; - * - the transport is backpressured — the socket is already queuing stream/ + * - the transport is backpressured - the socket is already queuing stream/ * control data, so a datagram behind it would arrive stale; or * - the datagram lane is full, bounding a synchronous burst that outruns the * writer loop even while the transport has room. @@ -154,7 +154,7 @@ export class SendScheduler { * applying backpressure to the caller. */ enqueueDatagram(bytes: Uint8Array): void { if (this.#closed) return; - if (!this.#sink.wantsMore()) return; // transport is queuing — drop, don't delay + if (!this.#sink.wantsMore()) return; // transport is queuing - drop, don't delay if (this.#datagrams.length >= DATAGRAM_QUEUE_LIMIT) return; // burst cap this.#datagrams.push(bytes); this.#signal(); @@ -266,7 +266,7 @@ export class SendScheduler { continue; } - // Block until the socket can take a write, THEN pick — so a frame + // Block until the socket can take a write, THEN pick - so a frame // that arrives during backpressure is considered before we commit. await this.#sink.ready(); diff --git a/js/qmux/src/session.test.ts b/js/qmux/src/session.test.ts index 75742459314..ad5d6c8f1a1 100644 --- a/js/qmux/src/session.test.ts +++ b/js/qmux/src/session.test.ts @@ -41,7 +41,7 @@ class FakePeer { sentAtClose: number | undefined; /** `options` mirrors the native `WebSocketStream` constructor, which - * `openWebSocketStream` calls with `{protocols, signal}` — hence `protocol` + * `openWebSocketStream` calls with `{protocols, signal}` - hence `protocol` * (the *negotiated* value, which only `Session.accept` tests set) rather than * a positional argument that call would fill in. */ constructor(url: string, options?: { protocol?: string }) { @@ -79,7 +79,7 @@ class FakePeer { this.sentAtClose ??= this.sent.length; this.#closedResolve(info); } - /** The socket errored out, rather than closing — `WebSocketStream.closed` rejects. */ + /** The socket errored out, rather than closing - `WebSocketStream.closed` rejects. */ error(err = new Error("socket error")) { this.#closedReject(err); } @@ -116,7 +116,7 @@ class FakePeer { sendText(text: string) { this.#recv.enqueue(text); } - /** Inject a raw binary record — bytes the encoder wouldn't normally emit + /** Inject a raw binary record - bytes the encoder wouldn't normally emit * (e.g. the no-length 0x30 datagram form). */ sendRaw(bytes: Uint8Array) { this.#recv.enqueue(bytes); @@ -140,7 +140,7 @@ class FakePeer { /** A plain `WebSocket` as a server host hands it over: already open, with the * subprotocol picked during the upgrade. `Session.accept` has to wrap this - * itself — there's no `WebSocketStream` in sight. */ + * itself - there's no `WebSocketStream` in sight. */ class FakeServerSocket implements WebSocketLike { binaryType = "blob"; bufferedAmount = 0; @@ -1165,7 +1165,7 @@ describe("Session integration (scripted peer)", () => { // The WebTransport contract: `closed` fulfills only on a graceful end, and rejects // with a WebTransportError (source "session") on an abnormal one. Without that - // split a consumer cannot tell a dropped session from a clean shutdown — the close + // split a consumer cannot tell a dropped session from a clean shutdown - the close // code can't carry it, since codes are application-defined. describe("closed settles graceful vs abnormal", () => { test("a peer APPLICATION_CLOSE resolves closed with its code and reason", async () => { @@ -1183,7 +1183,7 @@ describe("Session integration (scripted peer)", () => { await session.ready; // 1002 is a transport-error code, but an APPLICATION_CLOSE is graceful by - // definition — the *subtype*, not the code, decides. Codes are + // definition - the *subtype*, not the code, decides. Codes are // application-defined, so the app still gets its code and reason. peer.send({ type: "application_close", code: VarInt.from(1002), reason: "bye" }); expect(await session.closed).toEqual({ closeCode: 1002, reason: "bye" }); @@ -1194,7 +1194,7 @@ describe("Session integration (scripted peer)", () => { await session.ready; // The peer detected a protocol violation / transport error and sent a - // CONNECTION_CLOSE (0x1c) — abnormal, so `closed` rejects rather than + // CONNECTION_CLOSE (0x1c) - abnormal, so `closed` rejects rather than // fulfilling. Mirrors the frame we emit from #abort. peer.send({ type: "connection_close", @@ -1612,7 +1612,7 @@ describe("Session integration (scripted peer)", () => { test("datagrams: a DATAGRAM whose frame exactly fits our advertised size is delivered", async () => { // ourParams.maxDatagramFrameSize = 10; an 8-byte payload encodes to a - // 1 (type) + 1 (length varint) + 8 = 10-byte frame — exactly the limit. + // 1 (type) + 1 (length varint) + 8 = 10-byte frame - exactly the limit. const { session, peer } = connect({ maxDatagramFrameSize: 10n }); await session.ready; peer.send({ type: "transport_parameters", params: peerParams() }); @@ -1625,7 +1625,7 @@ describe("Session integration (scripted peer)", () => { test("datagrams: a DATAGRAM whose frame overflows our advertised size is fatal", async () => { // ourParams.maxDatagramFrameSize = 10. A 10-byte payload encodes to a - // 1 + 1 + 10 = 12-byte frame > 10 — a peer exceeding the size we negotiated + // 1 + 1 + 10 = 12-byte frame > 10 - a peer exceeding the size we negotiated // is a PROTOCOL_VIOLATION (RFC 9221), so the session must close, not drop. const { session, peer } = connect({ maxDatagramFrameSize: 10n }); await session.ready; @@ -1650,8 +1650,8 @@ describe("Session integration (scripted peer)", () => { test("datagrams: a no-length (0x30) DATAGRAM is sized without a length varint", async () => { // ourParams.maxDatagramFrameSize = 10. A 0x30 datagram carries no length - // varint, so a 9-byte payload is a 1 + 9 = 10-byte frame — exactly the - // limit — even though the length-prefixed reconstruction (1 + 1 + 9 = 11) + // varint, so a 9-byte payload is a 1 + 9 = 10-byte frame - exactly the + // limit - even though the length-prefixed reconstruction (1 + 1 + 9 = 11) // would wrongly drop it. Hand-build the record: a 0x30 type byte + payload. const { session, peer } = connect({ maxDatagramFrameSize: 10n }); await session.ready; @@ -1903,7 +1903,7 @@ describe("Session integration (scripted peer)", () => { session.close({ closeCode: 42, reason: "bye" }); // Peer-initiated streams still in flight when the session closed: one uni, - // one bidi — both would hit the now-closed incoming-stream controllers. + // one bidi - both would hit the now-closed incoming-stream controllers. // The third targets a stream id that could only exist pre-close (#recvStreams // was cleared by close), so it also takes the new-stream branch. peer.send({ @@ -1937,7 +1937,7 @@ describe("Session integration (scripted peer)", () => { // A client-initiated uni stream is send-only from our (client) perspective; // the peer writing to it is a protocol violation. Now that #handleStreamFrame - // is synchronous, the throw propagates to #onData and closes with 1002 — + // is synchronous, the throw propagates to #onData and closes with 1002 - // previously it leaked as a rejection while the session limped on. peer.send({ type: "stream", @@ -1967,7 +1967,7 @@ describe("Session integration (scripted peer)", () => { }); // Ping barrier: once the pong is seen the STREAM frame above has been fully - // processed. The session must still be alive — an app-local cancel is not a + // processed. The session must still be alive - an app-local cancel is not a // peer protocol violation. peer.send({ type: "ping_request", sequence: 1n }); await waitFor(() => peer.has("ping_response"), 5000); @@ -2018,7 +2018,7 @@ describe("Session integration (scripted peer)", () => { await session.incomingUnidirectionalStreams.cancel(); - // Five distinct refused uni streams carrying 500B total — well past the 300B + // Five distinct refused uni streams carrying 500B total - well past the 300B // window. With correct accounting the window keeps pace (MAX_DATA is emitted) // and the session stays healthy; the buggy refuse path froze `consumed` and // closed with a spurious flow-control error by the 4th stream. @@ -2257,7 +2257,7 @@ describe("Session.accept (server role)", () => { await session.ready; peer.send({ type: "transport_parameters", params: peerParams() }); - // Client-initiated (isServer: false) — the mirror of what a client session + // Client-initiated (isServer: false) - the mirror of what a client session // accepts, and what a client session would reject as a violation. peer.send({ type: "stream", @@ -2295,7 +2295,7 @@ describe("Session.accept (server role)", () => { test("ArrayBuffer chunks decode on a transport we were handed directly", async () => { // `openWebSocketStream` normalizes the native API's ArrayBuffer chunks, but - // accept() gets the transport as-is — and Chromium's WebSocketStream yields + // accept() gets the transport as-is - and Chromium's WebSocketStream yields // ArrayBuffer. Without normalization in the read loop this throws // "Expected ArrayBuffer for the first argument" out of the varint decoder // and the session never opens a stream. @@ -2380,8 +2380,8 @@ describe("STREAM frame size", () => { test("a record-sized STREAM frame is delivered", async () => { // A 5-byte STREAM header plus 16377 payload bytes is a record of exactly the // max_record_size we advertised: refusing it refuses the very size the - // handshake invited. A peer chunking at any other header budget — an older - // release, a third-party stack — lands somewhere in this range too. + // handshake invited. A peer chunking at any other header budget - an older + // release, a third-party stack - lands somewhere in this range too. const { session, peer } = connect(); await session.ready; peer.send({ type: "transport_parameters", params: peerParams() }); @@ -2398,7 +2398,7 @@ describe("STREAM frame size", () => { test("qmux-00 delivers a frame of exactly max_frame_size", async () => { // draft-00 §5.2 bounds the whole frame, header included, so a 16384-byte - // frame is legal — and the parameter may only ever be raised. + // frame is legal - and the parameter may only ever be raised. const { session, peer } = accept({ protocol: "qmux-00" }); await session.ready; @@ -2432,7 +2432,7 @@ describe("STREAM frame size", () => { test("a record over max_record_size closes the session", async () => { // The record is the limit a peer can actually derive from the handshake, so - // it is the one worth enforcing — and a record too large to parse is a + // it is the one worth enforcing - and a record too large to parse is a // connection-level violation. const { session, peer } = connect(); await session.ready; @@ -2461,7 +2461,7 @@ describe("STREAM frame size", () => { }); test("a peer advertising the largest max_record_size can still be written to", async () => { - // max_record_size is a varint, so 2^62-1 is legal — and is not a value + // max_record_size is a varint, so 2^62-1 is legal - and is not a value // `number` holds exactly, so the budget has to stay a bigint. const { session, peer } = connect(); await session.ready; diff --git a/js/qmux/src/session.ts b/js/qmux/src/session.ts index 8b8af9bf38c..412746f6cf9 100644 --- a/js/qmux/src/session.ts +++ b/js/qmux/src/session.ts @@ -399,7 +399,7 @@ export interface SessionOptions extends WebTransportOptions { * * The handshake is done by the time [[Session.accept]] is called, so the * subprotocol options ({@link SessionOptions.protocols}, `versions`, - * `requireProtocol`) don't apply — see [[selectSubprotocol]], which consumes + * `requireProtocol`) don't apply - see [[selectSubprotocol]], which consumes * them during the upgrade instead. */ export interface AcceptOptions extends Pick { @@ -408,7 +408,7 @@ export interface AcceptOptions extends Pick { // The socket went away without a CONNECTION_CLOSE. A peer close or a // local close() would already have transitioned us (and #close is - // idempotent), so reaching here settled means the session dropped — + // idempotent), so reaching here settled means the session dropped - // even if the WebSocket itself closed with a "clean" status code. this.#closeReason ??= new Error(`Connection closed: ${info.closeCode ?? 0} ${info.reason ?? ""}`); this.#abort(info.closeCode ?? 1006, info.reason ?? ""); }, (err: unknown) => { - // Keep the socket failure itself — it becomes the SessionError's cause, and + // Keep the socket failure itself - it becomes the SessionError's cause, and // it's the only description of *why* the transport died. this.#closeReason ??= err instanceof Error ? err : new Error("WebSocketStream closed"); this.#abort(1006, "WebSocketStream error"); @@ -867,7 +867,7 @@ export default class Session implements WebTransport { } // `openWebSocketStream` normalizes the native API's ArrayBuffer chunks, // but a transport handed straight to the constructor or [[accept]] - // hasn't been through it — and Chromium's WebSocketStream yields + // hasn't been through it - and Chromium's WebSocketStream yields // ArrayBuffer. Normalize here so every inbound path reaches the decoder // as a Uint8Array. this.#onData(value instanceof ArrayBuffer ? new Uint8Array(value) : value); @@ -886,7 +886,7 @@ export default class Session implements WebTransport { this.#protocol = parseProtocol(rawProtocol, version); // Datagrams rely on the record layer for framing, so don't advertise the - // parameter — or accept datagrams — on any non-record wire format. The recv + // parameter - or accept datagrams - on any non-record wire format. The recv // path gates on #ourParams.maxDatagramFrameSize. RESET_STREAM_AT is only // permitted on draft-02, so advertise `reset_stream_at` there and nowhere // else; the recv path gates on #ourParams.resetStreamAt. @@ -946,7 +946,7 @@ export default class Session implements WebTransport { } } else { // draft-00 §5.2: `max_frame_size` bounds the whole frame, header - // included, and may only be raised from its initial value — which we + // included, and may only be raised from its initial value - which we // never do, so the initial value is what a peer may send. Each message // is one frame here. if (this.#version === "qmux-00" && data.byteLength > Frame.MAX_FRAME_SIZE) { @@ -985,7 +985,7 @@ export default class Session implements WebTransport { } else if (frame.type === "application_close" || frame.type === "connection_close") { this.#closeReason ??= new Error(`Connection closed: ${frame.code.value} ${frame.reason}`); if (frame.type === "application_close") { - // APPLICATION_CLOSE (0x1d): the peer closed the session deliberately — + // APPLICATION_CLOSE (0x1d): the peer closed the session deliberately - // graceful, so `closed` fulfills with its code/reason. this.#close(Number(frame.code.value), frame.reason); } else { @@ -1009,7 +1009,7 @@ export default class Session implements WebTransport { } else if (frame.type === "datagram") { // `max_datagram_frame_size` is negotiated: we advertised the largest // DATAGRAM frame we're willing to receive, so a peer that sends one we - // never accepted — or one larger than we advertised — has violated the + // never accepted - or one larger than we advertised - has violated the // contract. Per RFC 9221 that's a fatal PROTOCOL_VIOLATION, not a // droppable frame; throwing here closes the session via #onData (matching // the Rust implementation's DatagramsUnsupported / FrameTooLarge). @@ -1126,7 +1126,7 @@ export default class Session implements WebTransport { this.#idleTimer = undefined; const timeoutMs = this.#effectiveIdleTimeoutMs(); if (timeoutMs === 0n) return; - // Poll at a fraction of the timeout — frequent enough to trigger pings on time + // Poll at a fraction of the timeout - frequent enough to trigger pings on time // but not so frequent it burns CPU on otherwise-quiet sessions. const timeout = Nano.fromMilli(Milli(Number(timeoutMs))); const timer = interval(Nano.fromMilli(Milli(Math.max(50, Number(timeoutMs) / 6)))); @@ -1164,7 +1164,7 @@ export default class Session implements WebTransport { * while its socket still accepts our writes would never be reclaimed. * * Crediting the first send after each receive is what still lets a mostly - * one-way sender stay open — its peer answers the keep-alive, and each answer + * one-way sender stay open - its peer answers the keep-alive, and each answer * re-arms the credit. */ #idleActivityAt(): Instant { @@ -1203,7 +1203,7 @@ export default class Session implements WebTransport { try { this.#sendPriorityFrame({ type: "ping_request", sequence: BigInt(seq) }); } catch (e) { - // Best effort — if the send fails, the close path will fire shortly. + // Best effort - if the send fails, the close path will fire shortly. // Log a breadcrumb so a never-fire encoder bug doesn't vanish silently. console.warn("qmux: keep-alive ping failed", e); } @@ -1511,7 +1511,7 @@ export default class Session implements WebTransport { } } } else { - // Existing stream — validate recv flow control + // Existing stream - validate recv flow control if (!this.#accountRecv(frame)) { this.#sendClose("connection_close", 1002, "flow control error"); this.#abort(1002, "flow control error"); @@ -1536,7 +1536,7 @@ export default class Session implements WebTransport { #handleResetStream(frame: Frame.ResetStream) { // A RESET_STREAM_AT frame (reliableSize present, draft-02) is only legal if // we advertised the `reset_stream_at` transport parameter. Receiving it - // otherwise — or on an earlier draft, which never advertises it — is a + // otherwise - or on an earlier draft, which never advertises it - is a // PROTOCOL_VIOLATION. Plain RESET_STREAM is always allowed. if (frame.reliableSize !== undefined && !this.#ourParams.resetStreamAt) { throw new Error("RESET_STREAM_AT received without advertising reset_stream_at"); @@ -1663,7 +1663,7 @@ export default class Session implements WebTransport { // biome-ignore lint/complexity/useMathMinMax: bigint operands; Math.min/max throw on bigint. const max = peerMax < compatibilityMax ? peerMax : compatibilityMax; // Unreachable with a conforming peer: max_record_size is validated against - // the draft-01 minimum on arrival. Guard anyway — a zero budget would make + // the draft-01 minimum on arrival. Guard anyway - a zero budget would make // the chunking loops below spin forever instead of failing. if (max === 0n) throw new Error("peer frame limit leaves no room for stream data"); return max; @@ -1749,7 +1749,7 @@ export default class Session implements WebTransport { await this.#enqueueStreamFrame(streamId, { type: "stream", id, offset, data: new Uint8Array(), fin: true }); } - /** Enqueue a DATAGRAM frame on the scheduler's bounded, lossy datagram lane — + /** Enqueue a DATAGRAM frame on the scheduler's bounded, lossy datagram lane - * dropped under transport backpressure or once closed, rather than piling up * on the (lossless, unbounded) control lane. Size/support checks happen in * {@link Datagrams} before we get here. */ @@ -1947,7 +1947,7 @@ export default class Session implements WebTransport { /** Shared teardown for both terminal transitions: tears down streams, * credits, and the scheduler. The caller has already marked the session - * closed and settled `ready`/`closed` — the only thing that differs between + * closed and settled `ready`/`closed` - the only thing that differs between * a clean shutdown and a dropped session is which way `closed` settled, and * {@link #close} vs {@link #abort} own that choice. */ #teardown() { @@ -2004,7 +2004,7 @@ export default class Session implements WebTransport { } /** Graceful terminal transition: `closed` **fulfills** with the close code - * and reason. Per the WebTransport contract this is the only clean outcome — + * and reason. Per the WebTransport contract this is the only clean outcome - * reached by a local {@link close} (which first puts an APPLICATION_CLOSE on * the wire) or by receiving a peer's APPLICATION_CLOSE (0x1d). A peer's * CONNECTION_CLOSE (0x1c) routes to {@link #abort} instead. Idempotent. */ @@ -2021,7 +2021,7 @@ export default class Session implements WebTransport { } /** Abnormal terminal transition: `closed` **rejects** with a - * `WebTransportError`. Reached on everything that is not a clean shutdown — + * `WebTransportError`. Reached on everything that is not a clean shutdown - * socket failure, read error, idle timeout, a protocol violation we detected * locally, or a peer's CONNECTION_CLOSE (0x1c). Close codes cannot carry this * distinction, since they are diff --git a/js/qmux/src/web-socket-stream.test.ts b/js/qmux/src/web-socket-stream.test.ts index 45ac2823a31..23ea5e2d6f5 100644 --- a/js/qmux/src/web-socket-stream.test.ts +++ b/js/qmux/src/web-socket-stream.test.ts @@ -137,7 +137,7 @@ describe("WebSocketStream ponyfill", () => { await tick(5); expect(settled).toBe(false); // backpressured at 100 - // Raise the mark above the buffered amount — the in-progress drain resolves + // Raise the mark above the buffered amount - the in-progress drain resolves // without bufferedAmount having to change. wss.setHighWaterMark(1000); expect(wss.highWaterMark).toBe(1000); diff --git a/js/qmux/src/web-socket-stream.ts b/js/qmux/src/web-socket-stream.ts index e00e24d6a0e..7d5505bfb2f 100644 --- a/js/qmux/src/web-socket-stream.ts +++ b/js/qmux/src/web-socket-stream.ts @@ -1,7 +1,7 @@ /** A ponyfill for the WHATWG [`WebSocketStream`][spec] API. * - * `WebSocketStream` exposes a WebSocket as a pair of streams — - * `{ readable, writable }` — with backpressure on the writable. It currently + * `WebSocketStream` exposes a WebSocket as a pair of streams - + * `{ readable, writable }` - with backpressure on the writable. It currently * ships only in Chromium. This module wraps a plain `WebSocket` to present the * same surface (`opened`, `closed`, `close()`), applying write backpressure by * polling `bufferedAmount` against a configurable high-water mark. @@ -57,7 +57,7 @@ export interface WebSocketStreamLike { /** Close the connection, with an optional code and reason. */ close(closeInfo?: WebSocketStreamCloseInfo): void; /** Ponyfill-only: resize the write-backpressure high-water mark at runtime. - * Absent on the native API, which sizes its own send buffer — callers should treat it as optional. */ + * Absent on the native API, which sizes its own send buffer - callers should treat it as optional. */ setHighWaterMark?(bytes: number): void; } @@ -143,7 +143,7 @@ function validCloseCode(code: number | undefined): code is number { /** Resolve once `bufferedAmount` drains below the mark (write backpressure). * The mark is read live via `highWaterMark()` on every check, so a runtime * resize (see {@link WebSocketStream.setHighWaterMark}) takes effect mid-drain. - * Returns `undefined` synchronously when there's already room — no microtask + * Returns `undefined` synchronously when there's already room - no microtask * on the hot path, so `writer.ready` stays resolved when the socket is keeping up. */ function drain(ws: WebSocketLike, highWaterMark: () => number): Promise | undefined { if (ws.bufferedAmount <= highWaterMark()) return undefined; @@ -239,7 +239,7 @@ export class WebSocketStream implements WebSocketStreamLike { }; // An adopted socket may already have finished (or missed) its handshake, in - // which case `onopen`/`onclose` will never fire — settle from readyState. + // which case `onopen`/`onclose` will never fire - settle from readyState. if (ws.readyState !== CONNECTING) { if (ws.readyState === OPEN) { opened.resolve({ readable, writable, extensions: ws.extensions, protocol: ws.protocol }); @@ -259,13 +259,13 @@ export class WebSocketStream implements WebSocketStreamLike { } } - /** Wrap a socket that already exists — typically one a server accepted from an + /** Wrap a socket that already exists - typically one a server accepted from an * HTTP upgrade (`Deno.upgradeWebSocket`, `ws`, ...), where there is no URL to * dial and the handshake (including subprotocol selection) is already done. * * Ownership transfers: this overwrites the socket's `onopen`/`onmessage`/ * `onerror`/`onclose` handlers, so adopt it before anything else reads from - * it — messages delivered before adoption are dropped by the platform, not + * it - messages delivered before adoption are dropped by the platform, not * buffered. A socket that is already open resolves {@link opened} without * waiting for an `onopen` that has already fired (or was never going to). * diff --git a/js/qmux/src/wire-format.test.ts b/js/qmux/src/wire-format.test.ts index 8345fd26c8b..02004a2e779 100644 --- a/js/qmux/src/wire-format.test.ts +++ b/js/qmux/src/wire-format.test.ts @@ -9,7 +9,7 @@ import { VarInt } from "./varint.ts"; // 1. The current decoder parses them into the expected Frame value. // 2. The current encoder produces the same bytes from the same Frame value. // -// Mirrors `rs/qmux/tests/wire_format.rs` — the Rust and TS implementations must +// Mirrors `rs/qmux/tests/wire_format.rs` - the Rust and TS implementations must // agree on the bytes, otherwise the JS polyfill can't interop with Rust peers. function bytes(...arr: number[]): Uint8Array { @@ -97,7 +97,7 @@ describe("QMux draft-00 wire format", () => { }); test("application_close", () => { - // 0x1d + code(=42) + reason_len(=3) + "bye". No Frame Type field — RFC 9000 + // 0x1d + code(=42) + reason_len(=3) + "bye". No Frame Type field - RFC 9000 // §19.19 omits it for the application variant. const wire = bytes(0x1d, 0x2a, 0x03, 0x62, 0x79, 0x65); const frame: Frame.ApplicationClose = { @@ -114,7 +114,7 @@ describe("QMux draft-00 wire format", () => { }); test("connection_close (transport, 0x1c)", () => { - // 0x1c + code(=42) + frame_type(=0) + reason_len(=3) + "bye" — a transport + // 0x1c + code(=42) + frame_type(=0) + reason_len(=3) + "bye" - a transport // error / protocol violation. The transport variant carries the Frame Type. const wire = bytes(0x1c, 0x2a, 0x00, 0x03, 0x62, 0x79, 0x65); const frame: Frame.ConnectionClose = { @@ -140,7 +140,7 @@ describe("QMux draft-00 wire format", () => { // `maxRecordSize` is seeded from DEFAULT_TRANSPORT_PARAMS on decode (16382), per draft-01. expect(decoded.params.maxRecordSize).toBe(Frame.DEFAULT_MAX_RECORD_SIZE); - // Re-encoding must round-trip. Clear maxRecordSize so the encoder skips it — + // Re-encoding must round-trip. Clear maxRecordSize so the encoder skips it - // a peer that didn't send the parameter wouldn't have it set, and the encoder // only writes non-zero params. const frame: Frame.TransportParameters = { diff --git a/js/signals/README.md b/js/signals/README.md index d6db9c3d931..48fcc380b72 100644 --- a/js/signals/README.md +++ b/js/signals/README.md @@ -131,7 +131,7 @@ age.set(21); // only the nested effect reruns: "You are 21 years old!" ### effect.abort An `AbortSignal` that is aborted when the effect reruns or closes. -Pass it to any API that accepts an `AbortSignal` — `fetch`, `addEventListener`, streams, etc. +Pass it to any API that accepts an `AbortSignal` - `fetch`, `addEventListener`, streams, etc. ```ts const url = new Signal("/api/data"); diff --git a/js/ui/src/graph.ts b/js/ui/src/graph.ts index cd543735b2a..00759c8bee1 100644 --- a/js/ui/src/graph.ts +++ b/js/ui/src/graph.ts @@ -141,7 +141,7 @@ export function graph(parent: Effect, title: string, opts?: GraphOptions): Graph const push = (v: number | undefined) => { samples.push(v !== undefined && Number.isFinite(v) ? Math.max(0, v) : 0); while (samples.length > capacity) samples.shift(); - value.textContent = v !== undefined && Number.isFinite(v) ? (opts?.format?.(v) ?? v.toFixed(0)) : "—"; + value.textContent = v !== undefined && Number.isFinite(v) ? (opts?.format?.(v) ?? v.toFixed(0)) : "-"; draw(); }; diff --git a/js/ui/src/stats.ts b/js/ui/src/stats.ts index 34f4a328122..0bd5a288941 100644 --- a/js/ui/src/stats.ts +++ b/js/ui/src/stats.ts @@ -32,7 +32,7 @@ export function statCard(kind: StatKind, label: string, svg: string): StatCard { /** Append a labeled row to a card's grid, returning the value span to fill in. */ export function statLine(grid: HTMLElement, label: string): HTMLSpanElement { const row = DOM.create("div", { className: "stat-line" }); - const value = DOM.create("span", { className: "stat-value" }, "—"); + const value = DOM.create("span", { className: "stat-value" }, "-"); row.append(DOM.create("span", { className: "stat-key" }, label), value); grid.appendChild(row); return value; diff --git a/js/watch/README.md b/js/watch/README.md index 75e6eb483c0..2b3f975cb3c 100644 --- a/js/watch/README.md +++ b/js/watch/README.md @@ -76,7 +76,7 @@ The simplest way to watch a stream: | `buffer` | duration | `0ms` | Future-dated media held before playback skips ahead. | | `captions` | string | off | Text rendition to render. | | `catalog-format` | hang, msf, manual | auto-detected | The catalog format; detected from the name suffix unless set. | -| `echo` | path segment | — | The viewer's feedback name: once the catalog solicits feedback, publish `/.echo` reporting what each rendition received and played. | +| `echo` | path segment | - | The viewer's feedback name: once the catalog solicits feedback, publish `/.echo` reporting what each rendition received and played. | Property and built-in UI changes reflect into attributes. Invalid URLs, numbers, durations, version lists, echo names, and rendering/catalog modes are refused: their property setters throw, and malformed markup diff --git a/js/watch/src/ui/components/latency.ts b/js/watch/src/ui/components/latency.ts index 423b30e2c6e..85c4b72a52f 100644 --- a/js/watch/src/ui/components/latency.ts +++ b/js/watch/src/ui/components/latency.ts @@ -45,7 +45,7 @@ export function latencyTab(parent: Effect, watch: MoqWatch): HTMLElement { // Numeric readout: the resolved delay. const readout = DOM.create("div", { className: "latency-readout" }); const delayStat = DOM.create("div", { className: "latency-stat" }); - const delayVal = DOM.create("span", { className: "latency-stat-value" }, "—"); + const delayVal = DOM.create("span", { className: "latency-stat-value" }, "-"); delayStat.append(DOM.create("span", { className: "latency-stat-label" }, "Delay"), delayVal); readout.append(delayStat); diff --git a/js/watch/src/ui/stats.ts b/js/watch/src/ui/stats.ts index 4e69d17930b..c86dcfadfe1 100644 --- a/js/watch/src/ui/stats.ts +++ b/js/watch/src/ui/stats.ts @@ -96,8 +96,8 @@ export function statsTab(parent: Effect, watch: MoqWatch): HTMLElement { const vStats = watch.video.out.stats.peek(); const w = vConf?.codedWidth ?? vCat?.display?.width; const h = vConf?.codedHeight ?? vCat?.display?.height; - vRes.textContent = w && h ? `${w}×${h}` : "—"; - vCodec.textContent = vConf?.codec ?? "—"; + vRes.textContent = w && h ? `${w}×${h}` : "-"; + vCodec.textContent = vConf?.codec ?? "-"; let fps: number | undefined; if (vStats && vPrev.frames > 0) { @@ -109,32 +109,32 @@ export function statsTab(parent: Effect, watch: MoqWatch): HTMLElement { vBitrateGraph.push(vBitrate); vFpsGraph.push(fps); if (vStats) vPrev = { frames: vStats.frameCount, bytes: vStats.bytesReceived, when: now }; - vLate.textContent = vStats ? `${vStats.late}` : "—"; - vStalls.textContent = vStats ? `${vStats.stalls} (${formatMillis(vStats.stalled)})` : "—"; + vLate.textContent = vStats ? `${vStats.late}` : "-"; + vStalls.textContent = vStats ? `${vStats.stalls} (${formatMillis(vStats.stalled)})` : "-"; // Audio. const aConf = watch.audio.source.out.config.peek(); const aStats = watch.audio.out.stats.peek(); - aCodec.textContent = aConf?.codec ?? "—"; - aRate2.textContent = aConf?.sampleRate ? formatHz(aConf.sampleRate) : "—"; - aChannels.textContent = aConf?.numberOfChannels ? `${aConf.numberOfChannels}` : "—"; + aCodec.textContent = aConf?.codec ?? "-"; + aRate2.textContent = aConf?.sampleRate ? formatHz(aConf.sampleRate) : "-"; + aChannels.textContent = aConf?.numberOfChannels ? `${aConf.numberOfChannels}` : "-"; const aBitrate2 = aStats ? rate(aPrev, aStats.bytesReceived, now) : undefined; - aBitrate.textContent = aBitrate2 !== undefined ? formatBitrate(aBitrate2) : "—"; + aBitrate.textContent = aBitrate2 !== undefined ? formatBitrate(aBitrate2) : "-"; if (aStats) aPrev = { bytes: aStats.bytesReceived, when: now }; // A non-zero count means the target is below what arrivals actually need. const aUnder = aStats?.underruns ?? 0; aUnderruns.textContent = watch.audio.out.stalled.peek() ? `${aUnder} (buffering)` : `${aUnder}`; - aLate.textContent = aStats ? `${aStats.late}` : "—"; + aLate.textContent = aStats ? `${aStats.late}` : "-"; // Network. "Estimated max" is the congestion controller / PROBE estimate; // "Actual" is the goodput we measure from the video + audio byte counters. const probe = watch.connection.probe.peek(); const estimate = probe?.estimatedRecvRate; - nMax.textContent = estimate ? formatBitrate(estimate) : "—"; + nMax.textContent = estimate ? formatBitrate(estimate) : "-"; const actual = vBitrate !== undefined || aBitrate2 !== undefined ? (vBitrate ?? 0) + (aBitrate2 ?? 0) : undefined; - nActual.textContent = actual !== undefined ? formatBitrate(actual) : "—"; + nActual.textContent = actual !== undefined ? formatBitrate(actual) : "-"; const rtt = probe?.rtt; nRttGraph.push(rtt !== undefined && rtt > 0 ? rtt : undefined); }, POLL); diff --git a/quest/m1/README.md b/quest/m1/README.md index ef568f55338..5ff74172317 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -186,7 +186,6 @@ per-frame CPU growth since #5926. - [Poll-first async](/quest/m1/poll-first/README.md) - library crates compile no coroutines by default: async wrappers are generated from `poll_*` and gated behind an `async` feature - [Authored tracks default to a 30 s max-age](/quest/m1/max-age-defaults.md) - raw and media tracks advertise 30 s unless their author chooses otherwise, with explicit unlimited in every binding - [HLS export lists a same-codec reconfigure's first group under the new init](/quest/m1/hls-reinit-same-codec.md) - the hand-over pick, whichever of the catalog and the records arrives first, after the catalog `hls` removal is planned -- [Gate the no-em-dash rule](/quest/m1/em-dash-gate.md) - `just check` fails on an em dash in any tracked text file - [The resume model passes its long run again](/quest/m1/resume-model-7542.md) - fix #7542's regression that drops groups a long-budget reader can still get - [A resume more than one group behind never hangs](/quest/m1/ietf-resume-behind.md) - a resumed read gets every group or a drop on every wire version - [moq-quic's idle connections keep no burst-sized state](/quest/m1/quic-readable-dedupe.md) - readable events are deduplicated and a quiet connection keeps no per-peak-stream bytes diff --git a/quest/m1/em-dash-gate.md b/quest/m1/em-dash-gate.md deleted file mode 100644 index 0117a6a9b9d..00000000000 --- a/quest/m1/em-dash-gate.md +++ /dev/null @@ -1,29 +0,0 @@ -# [S] Gate the no-em-dash rule - -## Goal - -`just check` and the landing gate fail on an em dash in any tracked text file -except the lockfiles, and the PR removes the current ones (213 in 50 -files, most in `js/qmux`, `rs/qmux`, and the UI stats panels), so `main` -passes. - -## Plan - -`AGENTS.md` says "No em dashes", but nothing enforces it. Adopted from -t3code, which turns its agent rules into lint rules, alongside moq.pro's -[matching gate](https://github.com/moq-dev/moq.pro/blob/main/quest/m1/rule-gate.md). - -- A `git grep` over the whole tree, run as a check beside - `check:large_files` in `sh/dispatch.sh`, on every diff. It costs - milliseconds and still catches an em dash that arrives through a merge. -- No allowlist: docs, drafts, tests, and UI copy are all covered. The fix is - a hyphen, comma, colon, or a new sentence. A UI empty-value placeholder - in the stats panels becomes a plain `-`. -- Rust's `allow_attributes_without_reason` already runs here, so the moq.pro - quest's other half has nothing to add upstream. - -Decided with the maintainer on 2026-10-31, in moq.pro's planning session: - -- An upstream gate: ✅ plan one in moq-dev/moq / keep the plan to moq.pro. -- Scope: ✅ all tracked text except lockfiles, the same scope as moq.pro. -- Milestone: ✅ m1 / m2. diff --git a/rs/moq-boy/src/emulator.rs b/rs/moq-boy/src/emulator.rs index 6911e199efc..d2f98be6bc9 100644 --- a/rs/moq-boy/src/emulator.rs +++ b/rs/moq-boy/src/emulator.rs @@ -2,7 +2,7 @@ //! //! Wraps the `boytacean` emulator library, adding per-viewer button tracking //! for crowd-control input. A button is pressed on the emulator as long as -//! at least one viewer is holding it (union semantics — "anarchy mode"). +//! at least one viewer is holding it (union semantics - "anarchy mode"). use anyhow::{Context, Result}; use std::collections::{HashMap, HashSet}; @@ -142,7 +142,7 @@ impl Emulator { self.sync_buttons(); } - /// A viewer disconnected — remove their button state. + /// A viewer disconnected - remove their button state. pub fn viewer_left(&mut self, viewer_id: &str) { self.viewers.remove(viewer_id); self.sync_buttons(); diff --git a/rs/moq-boy/src/main.rs b/rs/moq-boy/src/main.rs index 102cb7674ec..e56168d146e 100644 --- a/rs/moq-boy/src/main.rs +++ b/rs/moq-boy/src/main.rs @@ -118,7 +118,7 @@ impl Config { /// Shared state for a game session, accessible from multiple threads/tasks. /// -/// Everything here is either atomic, behind a mutex, or immutable — +/// Everything here is either atomic, behind a mutex, or immutable - /// safe to share via `Arc` between the emulator thread, /// track monitors, and async tasks. struct Session { diff --git a/rs/moq-ffi/README.md b/rs/moq-ffi/README.md index cfb759e7af5..b36cee565b9 100644 --- a/rs/moq-ffi/README.md +++ b/rs/moq-ffi/README.md @@ -43,11 +43,11 @@ The Dart generator is pinned in `flake.nix`; run `nix develop --command just dar ```text moq-ffi (this crate) -├── lib.rs — Library entry and UniFFI scaffolding -├── ffi.rs — FFI runtime and Abort helper -├── session.rs — QUIC session management -├── origin.rs — Broadcast routing (publish/consume) -├── consumer.rs — Catalog and track subscription -├── producer.rs — Broadcast and track publishing -└── error.rs — Error types +├── lib.rs - Library entry and UniFFI scaffolding +├── ffi.rs - FFI runtime and Abort helper +├── session.rs - QUIC session management +├── origin.rs - Broadcast routing (publish/consume) +├── consumer.rs - Catalog and track subscription +├── producer.rs - Broadcast and track publishing +└── error.rs - Error types ``` diff --git a/rs/moq-mux/README.md b/rs/moq-mux/README.md index 87732191eb5..2069b7bf545 100644 --- a/rs/moq-mux/README.md +++ b/rs/moq-mux/README.md @@ -9,7 +9,7 @@ # moq-mux Media muxers and demuxers for [Media over QUIC](https://moq.dev). Takes -containerized or raw-codec media in, produces a [hang](https://github.com/moq-dev/moq/tree/main/rs/hang) broadcast — or the other way around. +containerized or raw-codec media in, produces a [hang](https://github.com/moq-dev/moq/tree/main/rs/hang) broadcast - or the other way around. **Containers:** fMP4 / CMAF, MKV / WebM, MPEG-TS, FLV, LOC, hang Legacy. **Codecs:** H.264, H.265, AV1, AAC, Opus. diff --git a/rs/moq-mux/src/container/consumer.rs b/rs/moq-mux/src/container/consumer.rs index 045ca414f61..27c845a3e76 100644 --- a/rs/moq-mux/src/container/consumer.rs +++ b/rs/moq-mux/src/container/consumer.rs @@ -4212,7 +4212,7 @@ mod tests { frames }) .await - .expect("consumer hung — possible infinite loop regression"); + .expect("consumer hung - possible infinite loop regression"); assert_eq!(frames.len(), 3); finisher.await.expect("finisher task panicked"); @@ -4302,7 +4302,7 @@ mod tests { frames }) .await - .expect("consumer hung — max_timestamp regression"); + .expect("consumer hung - max_timestamp regression"); assert_eq!(frames.len(), 4, "Expected all 4 frames, got {}", frames.len()); assert_eq!(frames[0].timestamp, ts(0)); @@ -4704,7 +4704,7 @@ mod tests { // Group 0: finished normally, its first frame read before group 2 lands. write_group(&mut track, 0, &[ts(0), ts(20_000)]); assert_eq!(consumer.read_frame().await.unwrap().unwrap().timestamp, ts(0)); - // Group 2: finished (group 1 is missing — sequence gap) + // Group 2: finished (group 1 is missing - sequence gap) write_group(&mut track, 2, &[ts(200_000)]); track.finish().unwrap(); @@ -4923,7 +4923,7 @@ mod tests { let frames = kio::clock::timeout(Duration::from_secs(2), async { continuous_frames(&mut consumer).await }) .await - .expect("consumer hung — duration skip regression"); + .expect("consumer hung - duration skip regression"); assert_eq!(frames.len(), 2); assert_eq!(frames[0].timestamp, ts(0)); diff --git a/rs/moq-mux/src/container/fmp4/export_test.rs b/rs/moq-mux/src/container/fmp4/export_test.rs index 5f15bae7312..9c4d26571f6 100644 --- a/rs/moq-mux/src/container/fmp4/export_test.rs +++ b/rs/moq-mux/src/container/fmp4/export_test.rs @@ -78,7 +78,7 @@ async fn avc3_source_to_cmaf_export_roundtrip() { } /// Legacy AAC source (catalog `Container::Legacy`, codec `mp4a.40.2`, with a -/// `description` carrying the AudioSpecificConfig — the shape an MPEG-TS import +/// `description` carrying the AudioSpecificConfig - the shape an MPEG-TS import /// produces) → fMP4 export must synthesize an mp4a sample entry whose esds /// carries that AudioSpecificConfig, instead of bailing with UnsupportedSynthesis. #[tokio::test(start_paused = true)] diff --git a/rs/moq-mux/src/container/mkv/export.rs b/rs/moq-mux/src/container/mkv/export.rs index ff3541df7d7..2e45f293fcd 100644 --- a/rs/moq-mux/src/container/mkv/export.rs +++ b/rs/moq-mux/src/container/mkv/export.rs @@ -433,7 +433,7 @@ impl Export { } // 6. End-of-stream: flush any open cluster when every subscribed track - // has finished. We don't require catalog EOS here — a long-lived + // has finished. We don't require catalog EOS here - a long-lived // producer may keep the catalog open even after every active track has // ended, and we'd rather flush the cluster than hold the last frames. if !self.tracks.is_empty() && self.tracks.values().all(|t| t.finished) { @@ -787,7 +787,7 @@ impl Export { Some(cluster) => { let overflow = !cluster.fits(frame_ticks); // Roll on a video keyframe only once the cluster already has video - // frames in it — otherwise audio that arrived before the first + // frames in it - otherwise audio that arrived before the first // keyframe would split into its own (un-renderable) cluster. let gop_boundary = is_video && keyframe && cluster.has_video; let too_long = self diff --git a/rs/moq-mux/src/container/source.rs b/rs/moq-mux/src/container/source.rs index 60fd55b4b34..7a3c26f0bf3 100644 --- a/rs/moq-mux/src/container/source.rs +++ b/rs/moq-mux/src/container/source.rs @@ -130,7 +130,7 @@ pub(crate) struct ExportSource { after: Option, transform: Option, /// Resolved codec configuration record (avcC / hvcC / AudioSpecificConfig / - /// OpusHead). Some once the codec config is available — from the catalog + /// OpusHead). Some once the codec config is available - from the catalog /// `description`, or synthesized by the transform. description: Option, /// Video codec used to derive geometry from its configuration or keyframes. diff --git a/rs/moq-quic/src/tests/mod.rs b/rs/moq-quic/src/tests/mod.rs index 5de98e4bc1f..93b4b3c2105 100644 --- a/rs/moq-quic/src/tests/mod.rs +++ b/rs/moq-quic/src/tests/mod.rs @@ -2323,7 +2323,7 @@ fn connection_close_while_congestion_blocked() { ApplicationClose { error_code: VarInt(42), reason } ) if reason == REASON); // Close packets aren't congestion controlled and the test link has no latency, so the close - // should arrive the moment it was issued — any delay means a timer had to rescue it + // should arrive the moment it was issued - any delay means a timer had to rescue it assert_eq!(delivered_at, close_time); } diff --git a/rs/moq-sock/tests/udp/main.rs b/rs/moq-sock/tests/udp/main.rs index 9456a1c2e72..61dbf5cb87f 100644 --- a/rs/moq-sock/tests/udp/main.rs +++ b/rs/moq-sock/tests/udp/main.rs @@ -922,7 +922,7 @@ fn draining_error_queue_before_send_prevents_absorption() { )] std::thread::sleep(Duration::from_millis(20)); - // Drain the error queue before sending — this is the intended usage pattern + // Drain the error queue before sending - this is the intended usage pattern let mut got_error = false; for _ in 0..10 { match state.recv_transport_error((&sock).into()) { diff --git a/rs/moq-tokio/src/quic/web/session.rs b/rs/moq-tokio/src/quic/web/session.rs index 21faf43a7eb..4e0caf146ba 100644 --- a/rs/moq-tokio/src/quic/web/session.rs +++ b/rs/moq-tokio/src/quic/web/session.rs @@ -214,8 +214,8 @@ impl Session { /// Accept a new unidirectional stream. See [`crate::quic::driver::connection::Connection::accept_uni`]. pub async fn accept_uni(&self) -> Result { if let Some(accept) = &self.accept { - // `kio::wait` owns the waiter, so dropping this future — a `timeout` that - // expires, say — also drops its registration in `SessionAccept`. + // `kio::wait` owns the waiter, so dropping this future - a `timeout` that + // expires, say - also drops its registration in `SessionAccept`. kio::wait(|waiter| poll_accept_uni_shared(accept, waiter)) .await .map_err(|e| self.map_error(e)) @@ -697,7 +697,7 @@ pub struct SessionAccept { // Waiters from concurrent callers of accept_bi / accept_uni. // Every clone of the session polls this one struct, so an arrival has to be fanned - // out: each caller registers here and all of them are woken when a stream lands — + // out: each caller registers here and all of them are woken when a stream lands - // by the caller that saw it, once it has released the lock on this struct. bi_waiters: Fan, uni_waiters: Fan, @@ -762,7 +762,7 @@ impl SessionAccept { /// with [`kio::wait`], which holds the waiter inside the future it builds. /// /// A `Ready` here means every *other* parked accepter should be woken so it can - /// retry. This does not do that itself — see `poll_accept_uni_shared`, which wakes + /// retry. This does not do that itself - see `poll_accept_uni_shared`, which wakes /// them once the lock on this struct is released. // // Poll-based because we accept and decode streams in parallel. In async land this diff --git a/rs/moq-video/DESIGN-native-codecs.md b/rs/moq-video/DESIGN-native-codecs.md index c32cc03a37c..47b8f3096e8 100644 --- a/rs/moq-video/DESIGN-native-codecs.md +++ b/rs/moq-video/DESIGN-native-codecs.md @@ -262,7 +262,7 @@ A backend "fails to open" (driver missing, no device) the same way an ffmpeg - **Linux**: one binary that - `dlopen`s `libnvidia-encode` if an NVIDIA driver is present (no build dep), - - `dlopen`s `libva` for Intel/AMD (no build dep on libva-dev) — *intended*; + - `dlopen`s `libva` for Intel/AMD (no build dep on libva-dev) - *intended*; moq-vaapi 0.0.2 currently links libva instead, so this isn't realized yet (see #1837), - falls back to OpenH264 when its default feature is enabled and no GPU encoder is usable. diff --git a/rs/qmux/src/ws.rs b/rs/qmux/src/ws.rs index 6c3a56a12b0..d2f7a700ce2 100644 --- a/rs/qmux/src/ws.rs +++ b/rs/qmux/src/ws.rs @@ -13,7 +13,7 @@ use crate::{Config, Driver, Error, Session, Version, alpn}; /// The `tokio-tungstenite` version this crate is built against. /// /// [`Client::with_connector`] and [`Upgraded`] take `tokio-tungstenite` types -/// directly, so use this re-export rather than a separately pinned dependency — +/// directly, so use this re-export rather than a separately pinned dependency - /// a different minor version is a distinct crate to the compiler and fails to /// typecheck against these APIs. pub use tokio_tungstenite; @@ -52,7 +52,7 @@ fn capped( /// /// WebSocket has no built-in idle timeout: when the peer's host crashes /// or its network drops without sending a TCP FIN, the local socket -/// stays "open" until OS-level TCP keep_alive eventually probes — typically +/// stays "open" until OS-level TCP keep_alive eventually probes - typically /// hours. Set this to send periodic Pings and close the session if no /// frame arrives within `timeout`. #[derive(Debug, Clone, Copy)] diff --git a/sh/dispatch-query.test.py b/sh/dispatch-query.test.py index f8b4abbaab6..0b01299019f 100644 --- a/sh/dispatch-query.test.py +++ b/sh/dispatch-query.test.py @@ -282,7 +282,7 @@ def stub_ci(self, just=""): nix.chmod(0o755) self.env.update(IN_MOQ_DEV_SHELL="1", CALLS=str(self.root / "calls")) (self.root / "calls").unlink(missing_ok=True) - for script in ("markdown.sh", "shell.sh", "large-files.sh"): + for script in ("markdown.sh", "shell.sh", "large-files.sh", "text.sh"): path = self.repo / "sh" / script path.write_text("#!/bin/sh\nexit 0\n") path.chmod(0o755) @@ -295,6 +295,28 @@ def run_ci(self, base="origin/main"): self.assertEqual(result.returncode, 0, result.stderr) return (self.root / "calls").read_text().splitlines() + def test_check_and_ci_reject_em_dashes_outside_the_diff(self): + self.stub_ci() + shutil.copy(ROOT / "sh/text.sh", self.repo / "sh/text.sh") + self.change("doc/unchanged.md") + (self.repo / "doc/unchanged.md").write_text("old text \u2014 still checked\n") + self.git("add", ".") + self.git("commit", "-qm", "existing text") + self.git("update-ref", "refs/remotes/origin/main", "HEAD") + self.change("quest/m1/example.md") + for action in ("check", "ci"): + with self.subTest(action=action): + result = subprocess.run( + ["bash", "sh/dispatch.sh", action, "origin/main"], + cwd=self.repo, + env=self.env, + text=True, + capture_output=True, + ) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("doc/unchanged.md:1:", result.stdout) + self.assertIn(f"{action}: check text failed", result.stderr) + def test_ci_requires_its_tool_stubs_even_when_the_host_has_them(self): host = self.root / "host" host.mkdir() diff --git a/sh/dispatch.sh b/sh/dispatch.sh index 008873342d9..60494879ad8 100755 --- a/sh/dispatch.sh +++ b/sh/dispatch.sh @@ -192,6 +192,7 @@ module python '(\.pyi?|(^|/)pyproject\.toml)$' \ module shell '' \ tools='shellcheck shfmt' fix module large_files '' +module text '' module toml '' \ tools='taplo' fix module nix '' \ @@ -375,6 +376,7 @@ step() { sh/markdown.sh check ;; check:large_files) sh/large-files.sh "$large_base" ;; + check:text) sh/text.sh ;; check:python) files '*.py' '*.pyi' | xargs -0 ruff check --force-exclude files '*.py' '*.pyi' | xargs -0 ruff format --check --force-exclude diff --git a/sh/gh/release.sh b/sh/gh/release.sh index 8f6eb3a7e2b..f61e70a07d0 100755 --- a/sh/gh/release.sh +++ b/sh/gh/release.sh @@ -2,20 +2,20 @@ # # Shared release helpers for GitHub Actions workflows. # Usage: -# release.sh parse-version — extract SemVer from GITHUB_REF given a tag prefix -# release.sh prev-tag — find the tag immediately before the current one -# release.sh create — create or update a GitHub release with artifacts + SHA256SUMS -# release.sh git-tag-exists — check whether a tag exists on a remote repo -# release.sh read-version — read `version = "x.y.z"` from a manifest -# release.sh pypi-exists — check whether == is already on PyPI +# release.sh parse-version - extract SemVer from GITHUB_REF given a tag prefix +# release.sh prev-tag - find the tag immediately before the current one +# release.sh create - create or update a GitHub release with artifacts + checksum manifest +# release.sh git-tag-exists - check whether a tag exists on a remote repo +# release.sh read-version - read `version = "x.y.z"` from a manifest +# release.sh pypi-exists - check whether == is already on PyPI # release.sh maven-exists # release.sh ffi-unreleased # # Environment: -# GITHUB_REF — set by GitHub Actions (e.g. refs/tags/moq-relay-v1.2.3) -# GITHUB_OUTPUT — set by GitHub Actions (for writing step outputs) -# GH_TOKEN — required for `create` subcommand -# RELEASE_CHECKSUMS — manifest filename for `create` (default SHA256SUMS). Set +# GITHUB_REF - set by GitHub Actions (e.g. refs/tags/moq-relay-v1.2.3) +# GITHUB_OUTPUT - set by GitHub Actions (for writing step outputs) +# GH_TOKEN - required for `create` subcommand +# RELEASE_CHECKSUMS - manifest filename for `create` (default SHA256SUMS). Set # it when more than one workflow publishes into the same # release, so each writes its own manifest. @@ -96,7 +96,7 @@ sha256() { fi } -# Create or update a GitHub release with artifacts and their SHA256SUMS. +# Create or update a GitHub release with artifacts and their checksum manifest. # Args: # Reads tag/title/prev_tag from environment or step outputs. create_release() { diff --git a/sh/js/workspace.test.py b/sh/js/workspace.test.py index 354c056aae1..281fbeec173 100644 --- a/sh/js/workspace.test.py +++ b/sh/js/workspace.test.py @@ -76,7 +76,7 @@ def test_dispatcher_passes_only_js_impact_paths(self): GITHUB_STEP_SUMMARY=str(summary), ): env = dispatch_env(PATH=f"{tools}:{os.environ['PATH']}", ALLOW_HOST_TOOLCHAIN="1", SELECT_CALLS=str(calls)) - for script in ("markdown.sh", "shell.sh", "large-files.sh"): + for script in ("markdown.sh", "shell.sh", "large-files.sh", "text.sh"): path = sh / script path.write_text("#!/bin/sh\nexit 0\n") path.chmod(0o755) diff --git a/sh/text.sh b/sh/text.sh new file mode 100755 index 00000000000..65813a3ce71 --- /dev/null +++ b/sh/text.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Check every tracked text file, including files unchanged by the branch. +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +# Git decides what is binary. Lockfiles are generated dependency metadata. +# Spell the UTF-8 bytes as escapes so the match works in every locale. +# Matches stay on stdout. Stderr is captured because Git reports a file it +# cannot read there, yet still exits 1 when nothing else matches. +status=0 +{ errors="$(git --no-pager grep --no-textconv -n -I -F $'\xe2\x80\x94' -- . ':!*.lock' 2>&1 >&3 3>&-)" || status=$?; } 3>&1 +if [[ -n "$errors" ]]; then + printf '%s\n' "$errors" >&2 + echo "error: git grep could not scan every tracked file." >&2 + exit $((status > 1 ? status : 2)) +fi +case "$status" in + 0) + echo "error: em dashes are forbidden; replace them with plain punctuation (UI placeholders use '-')." >&2 + exit 1 + ;; + 1) exit 0 ;; + *) exit "$status" ;; +esac diff --git a/sh/text.test.py b/sh/text.test.py new file mode 100644 index 00000000000..4df5870d47a --- /dev/null +++ b/sh/text.test.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Exercise the tracked-text gate in a real Git repository.""" + +import os +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +EM_DASH = "\u2014" + + +class TextTests(unittest.TestCase): + def setUp(self): + (ROOT / ".tmp").mkdir(exist_ok=True) + self.temp = tempfile.TemporaryDirectory(dir=ROOT / ".tmp") + self.addCleanup(self.temp.cleanup) + self.repo = Path(self.temp.name) + (self.repo / "sh").mkdir() + shutil.copy(ROOT / "sh/text.sh", self.repo / "sh/text.sh") + self.env = dict(os.environ, LC_ALL="C", GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull) + self.git("init", "-q", "-b", "main") + self.git("config", "user.name", "test") + self.git("config", "user.email", "test@example.com") + self.git("config", "commit.gpgsign", "false") + self.git("add", ".") + self.git("commit", "-qm", "base") + + def git(self, *args): + return subprocess.run(["git", *args], cwd=self.repo, env=self.env, text=True, capture_output=True, check=True) + + def write(self, name, data): + path = self.repo / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data if isinstance(data, bytes) else data.encode()) + + def check(self, cwd=None): + return subprocess.run( + ["bash", str(self.repo / "sh/text.sh")], cwd=cwd or self.repo, env=self.env, text=True, capture_output=True + ) + + def test_plain_punctuation_and_other_unicode_pass(self): + self.write("text.md", "a - b, c: café (d).\n") + self.git("add", ".") + result = self.check() + self.assertEqual(result.returncode, 0, result.stderr) + + def test_reports_committed_staged_and_worktree_text(self): + self.write("old file.md", f"first line\nold {EM_DASH} text\n") + self.git("add", ".") + self.git("commit", "-qm", "old text") + self.write("staged.ts", f"// staged {EM_DASH} text\n") + self.git("add", ".") + self.write("sh/text.sh", (self.repo / "sh/text.sh").read_text() + f"# worktree {EM_DASH} text\n") + result = self.check(cwd=self.repo / "sh") + self.assertEqual(result.returncode, 1, result.stderr) + for location in ("old file.md:2:", "staged.ts:1:", "sh/text.sh:"): + self.assertIn(location, result.stdout) + self.assertIn("em dashes are forbidden", result.stderr) + + def test_binary_lockfiles_and_untracked_files_are_excluded(self): + self.write("binary.bin", b"\0" + EM_DASH.encode()) + for name in ("Cargo.lock", "nested/bun.lock", "nested/pubspec.lock", "flake.lock"): + self.write(name, EM_DASH) + self.git("add", ".") + self.write("untracked.md", EM_DASH) + self.write(".gitignore", "ignored.md\n") + self.write("ignored.md", EM_DASH) + self.git("add", ".gitignore") + result = self.check() + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "") + + def test_hidden_generated_and_extensionless_text_are_checked(self): + for name in (".github/workflow.yml", "AGENTS.md", "generated/output.ts", "clock.ts", "LICENSE"): + with self.subTest(name=name): + self.write(name, EM_DASH) + self.git("add", name) + result = self.check() + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn(f"{name}:1:", result.stdout) + self.git("rm", "-f", name) + + def test_git_errors_fail(self): + self.git("config", "grep.patternType", "invalid") + result = self.check() + self.assertNotEqual(result.returncode, 0) + self.assertIn("invalid", result.stderr) + + @unittest.skipIf(not hasattr(os, "geteuid") or os.geteuid() == 0, "needs POSIX file modes and a non-root user") + def test_unreadable_tracked_file_fails(self): + self.write("unreadable.md", "plain text\n") + self.git("add", ".") + path = self.repo / "unreadable.md" + path.chmod(0) + self.addCleanup(path.chmod, 0o644) + result = self.check() + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn("unreadable.md", result.stderr) + self.assertIn("could not scan", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/swift/.gitignore b/swift/.gitignore index 40a03d7ac31..aeab6ee1872 100644 --- a/swift/.gitignore +++ b/swift/.gitignore @@ -3,7 +3,7 @@ .swiftpm/ Package.resolved -# Generated by sh/swift/{check,package}.sh — never check these in. +# Generated by sh/swift/{check,package}.sh - never check these in. Sources/MoqFFI/Generated.swift Tests/MoqTests/Docs/Samples.swift MoqFFI.xcframework/ diff --git a/test/smoke/report/index.html b/test/smoke/report/index.html index 840a060fb97..e7c89c71ec0 100644 --- a/test/smoke/report/index.html +++ b/test/smoke/report/index.html @@ -316,12 +316,12 @@

History

if (s.total === 0) return `–`; return ``; }); - return `${scores.length ? scores.join("") : `—`}`; + return `${scores.length ? scores.join("") : `-`}`; }).join(""); const tag = g.required ? `required` : ""; return ` ${esc(g.name)}${tag}
${hosts.map(esc).join(", ")} - ${versions.length ? versions.map(esc).join("
") : `—`} + ${versions.length ? versions.map(esc).join("
") : `-`} ${cols}`; }).join(""); document.getElementById("latest").innerHTML = head + rows; diff --git a/test/smoke/token.sh b/test/smoke/token.sh index a2cfbf3f014..95eec1d54b1 100755 --- a/test/smoke/token.sh +++ b/test/smoke/token.sh @@ -28,7 +28,7 @@ JS_DIR="$SMOKE_DIR/clients/token/js" GENERATORS="rust" VERIFIERS="rust" # Cover both code paths in each impl: symmetric (HS256) and every asymmetric -# family they support — EdDSA (OKP/Ed25519), ES256 (EC), RS256 (RSA) — so the +# family they support - EdDSA (OKP/Ed25519), ES256 (EC), RS256 (RSA) - so the # default matrix (and `just token-full` / CI, which don't pass --algorithms) # can't silently stop exercising one. Override with --algorithms / TOKEN_ALGORITHMS. ALGORITHMS="${TOKEN_ALGORITHMS:-HS256,EdDSA,ES256,RS256}" diff --git a/test/ts/make-pending-eit.py b/test/ts/make-pending-eit.py index 8337c2143e0..1adca09c0c7 100755 --- a/test/ts/make-pending-eit.py +++ b/test/ts/make-pending-eit.py @@ -9,7 +9,7 @@ `tsp -P eitinject` cannot produce it: it re-derives present/following from the event list and stamps its own version, ignoring `version` and `current` in the input XML. So -this patches the generated stream instead — bumping the version and clearing +this patches the generated stream instead - bumping the version and clearing `current_next_indicator` over a trailing window, then recomputing the section CRC so the result is a legal stream and a rejection downstream means the guard fired rather than the section being malformed.