@minip2p/core
v0.11.9
Published
Platform-neutral TypeScript SDK for minip2p
Maintainers
Readme
@minip2p/core
Platform-neutral TypeScript SDK for minip2p.
Applications normally install a platform package such as @minip2p/react-native, which provides the native backend and re-exports these public types. Adapter authors can implement the small contract from @minip2p/core/backend.
The API provides typed events, cancellable Promise operations, and Stream handles:
const path = await endpoint.connect(remoteAddress);
// Optional: wait for Identify when you need advertised protocols.
await endpoint.waitPeerReady(path.peerId);
const rttMs = await endpoint.ping(path.peerId);
const stream = await endpoint.openStream(path.peerId, "/example/files/1");
await stream.write(new Uint8Array([1, 2, 3]));
stream.closeWrite();Reading applies backpressure: a chunk counts as consumed when read() returns it or every data handler has returned from it, and each stream delivers at most one receive window that has not been consumed, so a reader that stops reading stalls its sender instead of losing data. An async data handler is consumed when it returns its Promise, not when the Promise settles; use read() to hold the sender back while asynchronous work runs. Unconsumed bytes keep holding native resources after their connection ends (ADR 0012): native keeps the ended connection's slot until they are consumed or the stream is abandoned, so the SDK still acknowledges bytes it discards or reads after a close. An endpoint whose application never consumes what it received can have native refuse new connections.
The endpoint's event queue keeps stream and connection-end events. A peer-opened stream, or a connection, that ends without carrying data before any of its events was dispatched leaves the queue whole, so peer churn cannot grow it; an unseen connection that is replaced folds into its successor.
Streams are async iterables. Iteration preserves chunk order, drains bytes buffered before a remote write half-close, and then ends quietly. Local shutdown, reset, peer disconnect, connection replacement, or driver failure rejects the iterator with its terminal error:
for await (const chunk of stream) {
process(chunk);
}endpoint.events() returns an async iterable of endpoint events. Each iterator has a bounded, drop-oldest buffer. It reports dropped events in-band as queueOverflow events. The default cap is 4096; pass bufferCap to change it or signal to end iteration on abort.
for await (const event of endpoint.events({ signal })) {
if (event.type === "peerReady") {
console.log(event.peerId);
}
}A peer holds one connection at a time. connectionEstablished and connectionClosed mark the peer connecting and disconnecting, and connectionClosed carries a reason (ConnectionCloseReason: local, remote with the peer's code, timedOut, stalled, or error with a message); when a newer connection takes the peer's slot, a single connectionReplaced ({ peerId, oldConnId, newConnId }) stands in for both, the peer stays connected, and streams and pending opens on oldConnId end with StreamClosedError. peerReady carries the connId that completed Identify and fires once per connection. waitPeerReady follows the peer's current connection, so a stale peerReady for a replaced connection never resolves it; it resolves at once when connectionInfo(peerId).readyProtocols shows the current connection is already ready, and rejects with PeerDisconnectedError when that connection closes. When the connection's first Identify failed, the rejection is IdentifyFailedError, a PeerDisconnectedError with connId and kind (the IdentifyFailure). identifyFailed ({ peerId, connId, cause }, cause from IdentifyFailure: TimedOut, OpenFailed, Rejected, ClosedEarly, or Malformed) reports that failure; the connection never becomes ready, its connectionClosed follows, and Identify is not retried, so redial to try again.
connect accepts a Connection target: a peer ID, one complete peer multiaddress, or a list of complete addresses naming one peer. For a Connection attempt, a timeout ends only the local wait (cancelOnTimeout: true opts into cancelling), an aborted signal cancels the attempt (the wait settles from its terminal), and a terminal lost to event overflow rejects with ConnectResultLostError. Waits never stop unrelated events from reaching other subscribers.
Use endpoint.on("stream", handler) to claim inbound streams. Operations accept { timeoutMs, signal }; the default timeout is 65 seconds and timeoutMs: 0 disables it. Streams implement Symbol.dispose. endpoint.close(timeoutMs = 500) rejects outstanding operations at once, then resolves with an EndpointCloseReport ({ reason, outcome?, unfinished }) once native has disconnected peers and drained for up to timeoutMs (0 tears down at once); onClose observers receive the same report then. The deadline bounds teardown time, not delivery of accepted writes. Endpoints implement Symbol.asyncDispose (awaits close()) and Symbol.dispose (starts it).
limits overrides native transport limits: quic, tcp (with its own yamux), circuit (relay circuits' Yamux limits, independent of tcp.yamux), and maxCircuits. Every field is optional, and the SDK passes on only what you set, so the rest keep their native defaults. quic.connectionReceiveWindow takes "unlimited", quic.congestionController takes "cubic" (the default), "newReno" or "bbr", quic.packetThreshold (default 3) must be at least 3, quic.timeThreshold (a multiple of the RTT, default 1.125) must be between 1.0 and 10.0, and tcp.handshakeTimeoutMs and tcp.sendStallTimeoutMs take "never". Limits for a transport the endpoint does not bind are ignored. Native validates the rest: an invalid limit throws InvalidConfigError with capability Transport. @minip2p/web rejects limits.
const endpoint = Minip2p.create({
secretKey,
// TCP limits apply only when a TCP listen address binds TCP.
listen: ["/ip4/0.0.0.0/udp/0/quic-v1", "/ip4/0.0.0.0/tcp/0"],
limits: {
quic: {
streamReceiveWindow: 4_000_000,
connectionReceiveWindow: "unlimited",
},
tcp: { handshakeTimeoutMs: 10_000, yamux: { receiveWindow: 1 << 20 } },
},
});timeouts overrides the protocol deadlines, in milliseconds: negotiationMs (one outbound stream's protocol negotiation), identifyMs (the time a connection's first Identify may take, its negotiation included; the connection is closed if it fails or runs out) and pingMs (one ping, from opening its stream to the echo). negotiationMs and identifyMs default to 30 s and pingMs to 10 s; omitted fields keep their default. On a slow or loaded link, size them from how long each exchange actually takes there, plus slack for a retransmission. waitPeerReady's default timeout allows a connect plus identifyMs. A zero throws InvalidConfigError with capability Options. Unlike limits, timeouts applies in @minip2p/web too. The minip2p-rs README's Deadlines table lists every deadline and how they relate.
const endpoint = Minip2p.create({
secretKey,
timeouts: { negotiationMs: 45_000, identifyMs: 45_000 },
});create throws InvalidConfigError when native rejects the configuration. Its capability (Gossipsub, Mdns, Discovery, Transport, or Options, from ConfigCapability) names the part to change, and detail carries the native diagnostic. A malformed listen address and a socket the platform refused are not config errors; they keep their native error codes.
With mDNS enabled, mdnsFailed ({ kind, detail }, kind from MdnsFailureKind) reports a hard mDNS failure once per change in failure state. mDNS keeps running and retries, and mdnsRecovered follows once a retry succeeds; a goodbye that failed during shutdown is final.
Raw native unions, the Minip2pBackend adapter contract, resolveEndpointConfig (shared config defaults and validation), and typedFfiError (native FfiError variant to typed SDK error) are available from @minip2p/core/backend.
