---
name: udx-js
description: Use when writing TypeScript/Node.js code on js-udx: @go-ricochet/udx (listen, dial, UdxMultiplexer, UdxConnection, UdxStream) or the js-libp2p transport @go-ricochet/libp2p-udx (udx(), /udp/<port>/udx addresses, the ./multiaddr browser entry), including back-pressure, closing, MemoryNetwork tests and interop with go-udx and dart_udx.
triggers: js-udx, @go-ricochet/udx, @go-ricochet/libp2p-udx, @stephanfeb/udx, UdxMultiplexer, UdxConnection, UdxStream, MemoryNetwork, ManualClock, registerUdxProtocol, parseUdxMultiaddr, Protocol 768 was unknown
---

# js-udx

## What it is

js-udx is the TypeScript implementation of UDX (wire v3) for Node.js, a
QUIC-inspired reliable transport over UDP, in two npm packages:
`@go-ricochet/udx` (the protocol, no libp2p dependency) and
`@go-ricochet/libp2p-udx` (a js-libp2p v3 transport for `/udp/<port>/udx`).
It interoperates with go-udx and dart_udx, and the transport with
go-libp2p-udx-transport and dart-libp2p. Browsers have no raw UDP: in a
browser you can only register the `udx` multiaddr codec (see below). Use the
WebSockets or WebRTC transports to reach peers from a browser.

## Install

```sh
npm install @go-ricochet/udx                 # 0.1.0, Node >= 20, ESM only
npm install @go-ricochet/libp2p-udx libp2p @chainsafe/libp2p-noise @chainsafe/libp2p-yamux
                                             # 0.1.1, Node >= 22 (js-libp2p v3)
```

```ts
import { listen, dial, UdxMultiplexer, ErrorCode, ConnectionClosedError, StreamResetError } from '@go-ricochet/udx'
import { udx, UDX, parseUdxMultiaddr, toUdxMultiaddr } from '@go-ricochet/libp2p-udx'
import '@go-ricochet/libp2p-udx/multiaddr' // browser-safe: registers the codec only
```

The packages used to be `@stephanfeb/udx` and `@stephanfeb/libp2p-udx`; those
are deprecated, same code.

## Core workflow

### Echo server and client

```ts
import { dial, listen } from '@go-ricochet/udx'

const server = await listen({ host: '127.0.0.1', port: 0 })
server.on('connection', conn => {
  conn.on('stream', stream => {
    stream.on('data', chunk => stream.write(chunk))
    stream.on('end', () => stream.end())
  })
})
const { port } = server.address()

const conn = await dial(port, '127.0.0.1') // returns before the peer answers
const stream = conn.openStream()           // SYN goes out at once
stream.write(new TextEncoder().encode('hello, udx'))
stream.end()                               // half-close; we can still read

let reply = ''
for await (const chunk of stream) reply += new TextDecoder().decode(chunk, { stream: true })

conn.close()          // also closes the socket dial() made
await server.close()  // closes every connection, then the socket
```

### One socket that accepts and dials

```ts
const mux = await UdxMultiplexer.create({ host: '0.0.0.0', port: 4001 })
const conn = mux.dial(peerPort, peerHost)   // synchronous
await conn.established                      // optional: the peer answered
const inbound = await mux.accept()          // or mux.on('connection', ...)
const s = await inbound.acceptStream()      // or inbound.on('stream', ...)
```

### Writing with back-pressure

```ts
import type { UdxStream } from '@go-ricochet/udx'

async function send (stream: UdxStream, data: Uint8Array[]): Promise<void> {
  for (const chunk of data) {
    if (!stream.write(chunk)) {               // false at 256 KiB queued
      await new Promise<void>((resolve, reject) => {
        const onDrain = () => { stream.off('close', onClose); resolve() }
        const onClose = (err?: Error) => { stream.off('drain', onDrain); reject(err ?? new Error('closed')) }
        stream.once('drain', onDrain)
        stream.once('close', onClose)
      })
    }
  }
  stream.end()
}
```

### Closing without losing data

```ts
// 1. Wait until the receiver says it has everything (e.g. it ends its side).
for await (const _ of stream) {}
// 2. Wait until the peer has ACKed everything we sent, FINs included.
await conn.flushed()
// 3. Only now close: CONNECTION_CLOSE discards anything in flight or unread.
conn.close()
```

### Keep-alive and liveness

```ts
const mux = await UdxMultiplexer.create({ port: 0, keepAliveInterval: 10_000 })
const ok = await conn.ping()           // true when ACKed, false after 5000 ms
console.log(conn.smoothedRtt, conn.cwnd, conn.datagramSize)
```

### js-libp2p node

```ts
import { createLibp2p } from 'libp2p'
import { noise } from '@chainsafe/libp2p-noise'
import { yamux } from '@chainsafe/libp2p-yamux'
import { udx, UDX } from '@go-ricochet/libp2p-udx'

const node = await createLibp2p({
  addresses: { listen: ['/ip4/0.0.0.0/udp/0/udx'] },
  transports: [udx()],                 // udx({ keepAliveInterval, closeTimeout, inactivityTimeout, udx: {...} })
  connectionEncrypters: [noise()],
  streamMuxers: [yamux()]
})
const addr = node.getMultiaddrs().find(ma => UDX.exactMatch(ma))
```

### Browser: accept relay reservations that list /udx addresses

```ts
import '@go-ricochet/libp2p-udx/multiaddr'   // before createLibp2p
// or, without the dependency:
import { registry } from '@multiformats/multiaddr'
registry.addProtocol({ code: 0x0300, name: 'udx', size: 0 })
```

## Key concepts / API map

- `listen(opts)`: `Promise<UdxMultiplexer>` bound to `host`/`port` (0 picks one).
- `dial(port, host, opts?)`: `Promise<UdxConnection>` on its own socket, closed with the connection.
- `UdxMultiplexer.create(opts)`, `new UdxMultiplexer(socket, opts)`: one socket, routes by CID.
- `mux.dial(port, host)`, `accept()`, `close()`, `address()`, `connectionCount`, `closed`, `droppedDatagrams`; events `'connection'`, `'close'`.
- Options: `type`, `ipv6Only`, `recvBufferSize`, `sendBufferSize`, `streamOptions.writeHighWaterMark`, `keepAliveInterval`, `pmtud`, `antiAmplification`, `migration`, `versionNegotiation`, `statelessResetSecret`, `clock`.
- `UdxConnection`: `openStream()`, `acceptStream()`, `ping(ms?)`, `flushed()`, `close(code?, reason?)`, `established`; `smoothedRtt`, `cwnd`, `inflight`, `datagramSize`, `peerMaxData`, `streamCount`, `remoteAddress`, `remotePort`, `addressValidated`, `closed`; events `'stream'`, `'migrate'`, `'close'`.
- `UdxStream` (a Node `EventEmitter`, async iterable of `Uint8Array`): `write`, `end`, `reset(code)`, `pause`, `resume`; `writableLength`, `bufferedBytes`, `recvWindow`, `bytesWritten`, `bytesRead`, `writableEnded`, `readableEnded`, `isPaused`, `destroyed`, `error`; events `'data'`, `'end'`, `'drain'`, `'close'`.
- Errors: `ConnectionClosedError` (`code`, `reason`, `remote`), `StreamResetError` (`code`, `remote`), `WriteAfterEndError`, `UdxError`, `CodecError`; `ErrorCode.NoError` ... `ErrorCode.ConnectionTimeout` (0-6).
- Constants: `VERSION_CURRENT` (3), `SAFE_DATAGRAM_SIZE` 1200, `MAX_IDLE_TIMEOUT` 30 000, `MAX_INCOMING_STREAMS` 1024.
- Testing: `MemoryNetwork(clock, { delay, loss, reorder, reorderDelay, mtu }, seed)`, `ManualClock(start)` with `advance`/`runUntil`/`nextDeadline`, `seededRandom`.
- libp2p: `udx(init)`, `UdxTransport`, `UdxListener`, `UdxMultiaddrConnection`, `UDX` matcher, `parseUdxMultiaddr`, `toUdxMultiaddr`, `isUdxMultiaddr`, `registerUdxProtocol`, `UDX_PROTOCOL_CODE` (0x0300), `UDX_WIRE_VERSION`.

## Gotchas

1. **`write` returning false is a signal, not an error.** Ignore it and the
   queue grows without bound. Wait for `'drain'`, and also listen for
   `'close'` or the wait can hang forever.
2. **Streams start paused.** Nothing is emitted until a `'data'` listener is
   added, the stream is iterated, or `resume()` is called. Bytes count as
   consumed only when emitted, so an unread stream closes the peer's window.
3. **`conn.close()` throws data away.** CONNECTION_CLOSE drops data in flight
   and data the peer has but has not read. Use the end, `flushed()`, `close()`
   sequence above. `mux.close()` does not wait for `flushed()` either.
4. **A FIN does not mean "read".** `end()` says you finished sending. If you
   need to know the peer processed everything, have it reply or end its side.
5. **Idle connections close after 30 s** with `ErrorCode.ConnectionTimeout`.
   UDX sends nothing by itself; `keepAliveInterval` defaults to 0 in
   `@go-ricochet/udx` and to 10 000 in the libp2p transport. Values under
   15 000 survive one lost PING.
6. **Chunk boundaries are not preserved.** A stream is a byte stream; frame
   your messages.
7. **`'close'` fires once per connection or stream.** An unset argument on a
   connection means you closed it with code 0. Code 0 with `err.remote` is a
   normal remote close; code 6 is our idle timeout; code 1 with reason
   `'stateless reset'` means the peer lost the connection; code 4 can mean a
   version mismatch.
8. **Wire v3 only.** v2 and earlier never interoperate. js-udx answers an
   unknown version with a version negotiation packet, and a dialer that gets
   one before the peer answered closes at once instead of timing out.
9. **Interop minimums:** go-udx v0.1.3+ (older ones never ACK PINGs, so
   `ping()` resolves false), dart_udx 3.1.0+ (older ones merge a second
   stream into the first), go-libp2p-udx-transport v0.1.4+, dart-libp2p
   3.0.0+. Prefer go-udx v0.3.0 and dart_udx 4.3.0, which share the 1200-byte
   start and RFC 9002 recovery.
10. **Datagrams start at 1200 bytes** and PMTUD probes up to 1472 (1452 on
    IPv6), falling back on a black hole. Mobile carrier tunnels drop larger
    datagrams; keep `pmtud` on rather than sending large datagrams.
11. **Stream limits.** The peer allows 100 concurrent streams until it says
    otherwise (js-udx peers allow 1024); past it `openStream()` throws a
    `UdxError` with `ErrorCode.StreamLimitError`. go-udx counts every stream a
    connection ever had, so against go-udx a connection caps at 100 for life.
12. **Flow-control violation resets.** A peer that sends more than 8 MiB ahead
    of in-order data gets its stream reset with `ErrorCode.FlowControlError`.
13. **Stateless reset only works between js-udx peers.** Enabling
    `statelessResetSecret` is harmless with go-udx and dart_udx, which ignore
    the token. The secret must be 32 bytes or more and stable across restarts.
14. **`Protocol 768 was unknown` / `Protocol udx was unknown`.** The `udx`
    codec (0x0300) was not registered in the copy of `@multiformats/multiaddr`
    that js-libp2p uses. Import the package (or `/multiaddr` in a browser)
    before creating the node, and make sure only one copy of
    `@multiformats/multiaddr` (v13) is installed (`npm ls @multiformats/multiaddr`).
    Install packed tarballs, not linked directories, when testing from source.
15. **The package root does not bundle for the browser.** It imports
    `node:dgram`. Browser code may import only `@go-ricochet/libp2p-udx/multiaddr`.
16. **No DNS dialling.** The transport dials `/ip4` and `/ip6` only, not
    `/dns4`, `/dns6` or `/dnsaddr`. Each listen address gets its own socket;
    all dials share one socket per address family.
17. **Libp2p layering.** Each libp2p connection is one UDX connection whose
    first UDX stream is upgraded like TCP (multistream-select, Noise, Yamux),
    as go-libp2p-udx-transport and dart-libp2p do. Configure Noise and Yamux
    or you cannot reach those nodes.
18. **Do not pause libp2p streams you read to the end.** In `@libp2p/utils`
    7.4.1 a paused stream's iterator can end at the remote FIN with data still
    buffered (libp2p/js-libp2p#3646), including the `echo()` helper. Read with
    `for await` without pausing.
19. **Node reports IPv4 peers on a dual-stack socket as `::ffff:a.b.c.d`.**
    `toUdxMultiaddr` maps them back to `/ip4`; do the same in your own code.

## Testing

- Deterministic in-process network:

```ts
import { ManualClock, MemoryNetwork, UdxMultiplexer } from '@go-ricochet/udx'

const clock = new ManualClock(1_000_000)
const net = new MemoryNetwork(clock, { delay: 5, loss: 0.01, mtu: 1350 }, 1)
const server = new UdxMultiplexer(net.createSocket('10.0.0.1', 9000), { clock })
const client = new UdxMultiplexer(net.createSocket('10.0.0.2', 9000), { clock })
const conn = client.dial(9000, '10.0.0.1')
let established = false
void conn.established.then(() => { established = true })
const done = (): boolean => established // your own completion check
// Drive time: fire due timers, then let promises settle, until done.
while (!done()) {
  const next = clock.nextDeadline()
  if (next !== undefined) clock.runUntil(next)
  await new Promise(resolve => setImmediate(resolve))
}
```

  `packages/udx/test/helpers/net.ts` has `memoryPair`, `runUntil`, `advance`
  and `collect` helpers built this way; `net.rebind(...)` simulates a NAT
  moving a peer.
- Real UDP: `listen({ host: '127.0.0.1', port: 0 })` plus `dial`; close both in `afterEach`.
- Repo commands: `npm test` (vitest), `npm run ci` (typecheck, build, lint, test).
- Interop suites run when sibling checkouts exist (`../go-udx`, `../dart-udx`,
  `../go-libp2p-udx-transport`, `../dart-libp2p`) and skip otherwise;
  `npm run interop:go-udx` runs go-udx's bulk suites against js-udx;
  `UDX_SLOW_TESTS=1` adds 45 s idle keep-alive tests.
- Codec changes must still match `packages/udx/test/vectors/go-v3.json`
  (regenerate with `npm run vectors` after a go-udx wire change).
- `packages/libp2p-udx/test/compliance.test.ts` runs js-libp2p's transport compliance suite.

## Red lines

- go-udx is the wire authority. Never change encodings, frame codes or the
  version in js-udx alone; codec output must stay byte-identical to the
  go-udx vectors.
- Never change the multiaddr code: `udx` is 0x0300 with size 0 in all three stacks.
- Never import the package root into browser bundles.
- Never call `conn.close()` right after writing data you need delivered;
  `await conn.flushed()` first.
- Close every multiplexer you create (`await mux.close()`); each owns a UDP
  socket and timers that keep Node running.
- UDX itself is unencrypted. Outside libp2p (which adds Noise), add your own
  security layer before sending anything sensitive.

## Reference

- Docs: https://cryptopeer.net/udx/js/ (getting-started, streams, connections, libp2p, interop pages)
- Protocol overview: https://cryptopeer.net/udx/
- Repo: https://github.com/stephanfeb/js-udx (PLAN.md has the wire rules and deviations)
- npm: https://www.npmjs.com/package/@go-ricochet/udx, https://www.npmjs.com/package/@go-ricochet/libp2p-udx
- Related skills: udx-go, udx-dart, go-libp2p-udx
