# @moq/qmux

> QMux protocol (draft-ietf-quic-qmux-02) over WebSockets

Latest version **0.3.3** (published 2026-08-06) · (MIT OR Apache-2.0) license · 0 weekly downloads

## Install

```sh
npm install @moq/qmux
pnpm add @moq/qmux
yarn add @moq/qmux
bun add @moq/qmux
```

## Health

**Score 70/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; has provenance; recently updated; high maintenance score.

Warnings: low downloads; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.3.3 |
| Published | 2026-08-06 |
| First published | 2026-03-13 |
| Weekly downloads | 0 |
| License | (MIT OR Apache-2.0) |
| TypeScript types | bundled |
| Module format | ESM |
| Dependencies | 1 |
| Unpacked size | 290.1 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Provenance | attested (GitHub Actions) |
| Author | Luke Curley |
| Maintainers | kixelated |
| Keywords | qmux, webtransport, websocket, polyfill, quic, streams |

## Links

- npm: https://www.npmjs.com/package/@moq/qmux
- Repository: https://github.com/moq-dev/web-transport
- Homepage: https://github.com/moq-dev/web-transport#readme
- Issues: https://github.com/moq-dev/web-transport/issues
- npm.io page: https://npm.io/package/@moq/qmux

## Dependencies (1)

- [@moq/web-socket-stream](https://npm.io/package/@moq/web-socket-stream.md) ^0.1.1

## Alternatives

- [@opentelemetry/exporter-zipkin](https://npm.io/package/@opentelemetry/exporter-zipkin.md) — 14.8M weekly downloads
- [pusher-js](https://npm.io/package/pusher-js.md) — 2.0M weekly downloads
- [browserify](https://npm.io/package/browserify.md) — 1.7M weekly downloads
- [sqs-consumer](https://npm.io/package/sqs-consumer.md) — 1.7M weekly downloads
- [@sanity/eventsource](https://npm.io/package/@sanity/eventsource.md) — 930.8K weekly downloads

## Recent versions

- 0.3.3 (latest) — 2026-08-06
- 0.3.2 — 2026-07-27
- 0.3.1 — 2026-07-21
- 0.3.0 — 2026-07-13
- 0.2.0 — 2026-07-03
- 0.1.3 — 2026-06-15
- 0.1.2 — 2026-06-08
- 0.1.1 — 2026-05-30
- 0.1.0 — 2026-05-28
- 0.0.6 — 2026-03-17
- 0.0.5 — 2026-03-17
- 0.0.4 — 2026-03-17
- 0.0.3 — 2026-03-16
- 0.0.1 — 2026-03-13

## README

# @moq/qmux

A [WebTransport](https://developer.mozilla.org/en-US/docs/Web/API/WebTransport_API) implementation using WebSockets as the underlying transport with [QMux](https://www.ietf.org/archive/id/draft-ietf-quic-qmux-02.html) (draft-ietf-quic-qmux-02, negotiating down to draft-01 and draft-00) framing: a polyfill in browsers, and a server on any runtime that can accept a WebSocket.

QMux brings QUIC's multiplexed streams and flow control to reliable, ordered byte-stream transports like WebSockets. This allows WebTransport applications to seamlessly fall back when QUIC/UDP is blocked by network middleboxes.

## Install

```bash
npm install @moq/qmux
```

## Usage

Use as a drop-in `WebTransport` replacement:

```ts
import Session from "@moq/qmux"

const transport = new Session("https://example.com/endpoint")
await transport.ready

const stream = await transport.createBidirectionalStream()
```

### 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
the value to accept from what the client offered, and the session reads the wire-format version back
off the socket.

```ts
import Session, { selectSubprotocol } from "@moq/qmux"

Deno.serve((req) => {
	const protocol = selectSubprotocol(req.headers.get("sec-websocket-protocol"), {
		protocols: ["moq-lite-04"],
		versions: { "moq-lite-04": null },
	})
	if (!protocol) return new Response("no supported protocol", { status: 400 })

	const { socket, response } = Deno.upgradeWebSocket(req, { protocol })
	const session = Session.accept(socket)
	handle(session) // a WebTransport, same as the client side
	return response
})
```

Any already-accepted `WebSocket` works — `Deno.upgradeWebSocket`, Node's `ws`, or a
`WebSocketStream`. Accepting the upgrade without a negotiated subprotocol falls back to the legacy
`webtransport` wire format, so reject the request when `selectSubprotocol` returns `undefined`
rather than accepting a socket the peer's framing won't match. If your host doesn't expose the
negotiated value on the socket, pass it explicitly as `{ protocol }`.

The result is an ordinary `WebTransport`: the only difference from a client session is which half of
the stream-id space each side owns.

### Detecting a dropped session

`closed` follows the WebTransport contract:

- **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
  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.

```ts
try {
	const info = await transport.closed
	console.log("closed gracefully", info.closeCode, info.reason)
} catch (err) {
	// err is a WebTransportError-shaped SessionError: err.source === "session"
	console.warn("session dropped, reconnecting", err)
}
```

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.

### Stream reset codes

A reset carries an application error code in both directions, like native WebTransport:

```ts
import { StreamError } from "@moq/qmux"

// Send RESET_STREAM with code 26.
await stream.writable.abort(new StreamError(26))

// Receive one: the code is a field, not text buried in the message.
try {
	await reader.read()
} catch (err) {
	// err.source === "stream"
	console.warn("reset by peer with code", err.streamErrorCode)
}
```

The outgoing code comes off the reason passed to `abort()` / `cancel()`, so any `WebTransportError`
(native or the exported `StreamError`) sends its `streamErrorCode` and anything else sends 0.
`STOP_SENDING` works the same way, erroring the sender's writable with the code the peer chose.

### Polyfill

Install as a global `WebTransport` polyfill:

```ts
import { install } from "@moq/qmux"

// Only installs if native WebTransport is unavailable
install()

// Now use the standard WebTransport API
const transport = new WebTransport("https://example.com/endpoint")
```

## License

Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT license](LICENSE-MIT) at your option.

---
_Source: https://npm.io/package/@moq/qmux · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
