DataChannel
A thin backpressure-aware wrapper around an RTCDataChannel. Use this when you've opened a DC via the remote.pc.createDataChannel(...) escape hatch and need to send more bytes per second than the buffer can sustain.
You don't need this for occasional small messages — call dc.send(...) on the raw RTCDataChannel directly. You need it when you're streaming continuously (telemetry, file transfer, game state at 60 Hz) and could outpace the channel's send queue.
Why backpressure matters
Browsers don't return an error when you dc.send(...) faster than the channel can flush. They silently buffer. If the buffer grows unboundedly, the page memory keeps climbing and eventually the tab crashes. The fix is to throttle sends against dc.bufferedAmount, which is exactly what this wrapper does.
Construct
import { DataChannel } from "@metered-ca/peer";
remote.on("state-change", ({ to }) => {
if (to !== "connected") return;
const raw = remote.pc.createDataChannel("file-transfer");
const dc = new DataChannel(raw, {
maxBufferedAmount: 1_048_576, // pause sending if buffer > 1 MiB (default)
maxQueuedSends: 256, // reject if 256+ sends are already queued
});
// …use dc.send below
});
Options
| Option | Type | Default | Notes |
|---|---|---|---|
maxBufferedAmount | number | 1_048_576 (1 MiB) | When bufferedAmount exceeds this, send() suspends. The wrapper listens for bufferedAmountLow to resume. |
bufferedAmountLowThreshold | number | maxBufferedAmount / 2 | Resume threshold. The wrapper sets dc.bufferedAmountLowThreshold to this value. |
maxQueuedSends | number | 256 | Hard cap on pending sends. If you call send() while this many are queued, it rejects with DataChannelOverflowError. |
logger | Logger | NoopLogger | Optional. |
Methods
send(data) → Promise<void>
Queues a send. Suspends if bufferedAmount > maxBufferedAmount; resumes when the buffer drains below bufferedAmountLowThreshold. Chain-sequenced — concurrent calls won't burst the buffer.
for await (const chunk of fileChunks) {
await dc.send(chunk); // backpressure-aware
}
data is string | ArrayBuffer. For typed arrays, use chunk.buffer.
Rejects with:
DataChannelOverflowErrorifmaxQueuedSendsis exceeded — your producer is faster than the network. Either throttle upstream or raisemaxQueuedSends.- Underlying RTCDataChannel error if the channel closes mid-send.
import { DataChannelOverflowError } from "@metered-ca/peer";
try {
await dc.send(chunk);
} catch (e) {
if (e instanceof DataChannelOverflowError) {
// queued = current pending count; cap = your maxQueuedSends
pauseProducer();
} else throw e;
}
close() → void
Closes the underlying RTCDataChannel and detaches listeners. Idempotent. Use this when you're done with the channel; reading peers will fire their close event.
Read-only state
dc.label // string — the label you passed to createDataChannel
dc.readyState // "connecting" | "open" | "closing" | "closed"
dc.bufferedAmount // current bytes pending in the underlying buffer
Events
dc.on("open", () => …);
dc.on("close", () => …);
dc.on("message", (ev) => handle(ev.data));
dc.on("error", (ev) => …);
Same events the underlying RTCDataChannel emits — the wrapper passes them through.
What survives a reconnect
Nothing. The DataChannel wraps an RTCDataChannel, which is tied to the RTCPeerConnection it was opened on. When the signalling WS reconnects, the SDK swaps each survivor's PC, which closes every DataChannel on the old PC.
The right pattern: re-construct the wrapper on each state-change → "connected" event:
const channels = new Map(); // peerId → DataChannel
peer.on("peer-joined", ({ peer: remote }) => {
remote.on("state-change", ({ to }) => {
if (to !== "connected") return;
channels.get(remote.id)?.close();
const raw = remote.pc.createDataChannel("file-transfer");
const dc = new DataChannel(raw, { maxBufferedAmount: 1_048_576 });
dc.on("message", (e) => handle(e.data));
channels.set(remote.id, dc);
});
});
peer.on("peer-left", ({ peer: remote }) => {
channels.get(remote.id)?.close();
channels.delete(remote.id);
});
See Data Channels & Low Latency for the full pattern including reconnect-aware producer pause/resume.
When NOT to use this wrapper
- Occasional messages (chat, low-frequency events). Just
dc.send(...)on the rawRTCDataChannel. No need for queueing. - Server-routed messages.
peer.send(data)already has its own queueing + ack flow at the wire layer. The DataChannel wrapper is specifically for P2P sends. - Reliable file transfer with chunk-level retry. This wrapper handles backpressure, not retry-on-failure. If the channel closes mid-transfer, you'll need application-level resume logic.
See also
RemotePeer—remote.pcis where you callcreateDataChannel- Data Channels & Low Latency — full guide with end-to-end example
- Reconnect Best Practices