Skip to content
minip2p
Esc
navigateopen⌘Jpreview
On this page

Connections and streams

Connect with cancellable Promises, inspect direct or relayed paths, and exchange bytes over custom protocol streams.

The React Native SDK separates finding a usable network path from opening an application protocol. Connect first, wait for Identify readiness, then open a negotiated stream.

Connect with a Promise

Use connectAddr when you have one complete peer multiaddress:

const result = await endpoint.connectAddr(remoteAddress, {
  timeoutMs: 15_000,
});

await endpoint.waitPeerReady(result.peerId, {
  timeoutMs: 10_000,
});

console.log(result.connectId, result.path);
Input you have Method
Peer ID already in the discovery book connect(peerId, options?)
Peer ID plus an ordered candidate list connectWithAddrs(peerId, addresses, options?)
One address ending in /p2p/<peer-id> connectAddr(address, options?)

All three resolve with { connectId, peerId, path }. The first path may be directDialed, directPunched, or relayed. Call path(peerId) when you need the current answer; a relayed connection can upgrade after the original Promise resolves.

The lower-level dial, dialIp4, and dialIp6 methods only start direct QUIC dials and return connection IDs immediately. They do not run the relay race or return a Path. Observe peerReady or call waitPeerReady afterward.

Timeouts and cancellation

connect*, waitPeerReady, ping, openStream, once, and waitFor accept:

interface OpOptions {
  timeoutMs?: number;
  signal?: AbortSignal;
}

The default timeout is 65 seconds. Set timeoutMs: 0 only when a caller truly wants an unbounded wait.

const controller = new AbortController();

const connectPromise = endpoint.connectAddr(remoteAddress, {
  signal: controller.signal,
  timeoutMs: 20_000,
});

// A screen transition or user action can stop this attempt.
controller.abort();
await connectPromise; // rejects with AbortError

For a connect* Promise, an abort or timeout cancels the underlying connection attempt. Concurrent ping calls to the same peer share one native Ping while each caller keeps its own timeout and abort signal.

Use the split-phase API when the UI must retain the connection-attempt ID:

const connectId = endpoint.startConnectAddr(remoteAddress);
const resultPromise = endpoint.waitConnectResult(connectId, {
  timeoutMs: 20_000,
});

// A separate UI action can cancel by ID.
cancelButton.onclick = () => endpoint.cancelConnect(connectId);

const result = await resultPromise;
console.log(result.path);

One terminal result can be consumed once. Waiting twice, or waiting for an unknown attempt, rejects with ConnectResultUnavailableError.

Register a protocol

Protocol IDs are versioned strings owned by your application. Register them in the initial configuration or before a remote peer opens an inbound stream.

const endpoint = Minip2p.create({
  secretKey,
  protocols: ["/example/files/1"],
});

endpoint.addProtocol("/example/chat/1");

Both peers must support the same ID. Wait for peerReady before opening so the remote Identify snapshot is available.

Open and write

const stream = await endpoint.openStream(
  peerId,
  "/example/files/1",
  { timeoutMs: 10_000 }
);

stream.write("header\n");
stream.write(new Uint8Array([1, 2, 3]));
stream.closeWrite();

write accepts UTF-8 text, Uint8Array, or ArrayBuffer. closeWrite() half-closes only the local write side; reads stay available until the remote half-closes or the stream terminates.

Claim inbound streams

A named stream handler receives the live handle:

const removeInbound = endpoint.on("stream", (stream) => {
  if (stream.protocolId !== "/example/files/1") {
    stream.abandon();
    return;
  }

  void receiveFile(stream);
});

Unclaimed inbound streams are abandoned. A catch-all event subscriber receives only inboundStream metadata and cannot claim the live handle.

Choose one read mode

Pull mode is natural for framed or sequential decoders:

import type { Stream } from "@minip2p/react-native";

async function receiveFile(stream: Stream) {
  try {
    for (;;) {
      const chunk = await stream.read();
      if (chunk === undefined) break;
      consume(chunk);
    }
    stream.closeWrite();
  } catch (error) {
    stream.reset();
    throw error;
  }
}

Flowing mode delivers chunks to a handler:

const removeData = stream.on("data", (chunk) => consume(chunk));
const removeRemoteClose = stream.on("remoteWriteClosed", () => {
  removeData();
  finishIncomingMessage();
});

// Call this if the owner unmounts first; both callbacks are idempotent.
const stopFlowing = () => {
  removeData();
  removeRemoteClose();
};

Pull reads and flowing data handlers are mutually exclusive. Calling read() after subscribing to data, or subscribing to data after the first read(), rejects or throws.

End a stream deliberately

Operation Result
closeWrite() Graceful local half-close; reading remains possible.
reset() Abruptly reset the stream and emit closed.
abandon() Reset, relinquish the handle, and suppress later native events.
stream[Symbol.dispose]() Equivalent to abandon().

remoteWriteClosed means no more inbound bytes will arrive. closed is the terminal lifecycle event. Pull reads use a bounded buffer; if the application does not keep up, dataOverflow reports dropped chunks and bytes. Treat that as a protocol-level recovery condition rather than silently continuing a framed transfer.

Next: Networking and events.

Last updated on July 31, 2026

Was this page helpful?