The protocol negotiation docs explain how a connection is upgraded for encryption and muxing, then it explains how application protocols are negotiated on the muxed streams. Why is this second part separated and treated differently from the first? Is it some sort of architectual model? It seems to me that separating connection upgrades as libp2p modules is an unnecessary abstraction that limits the user’s choices.
For example what if the user didn’t really care about encryption but they wanted to keep the muxer? An example of this use case is unix domain sockets. (I know we could use a plain text encryption module but that is besides the point.) What about if the user wants something additional layered in the system like a compression algorithm?
Some sudo-code to help explain what I was expecting the code to look like:
const connection = await node.dial(peer);
await connection.upgrade(["/noise", "/tls"]);
await connection.upgrade(["/gzip"]);
await connection.upgrade(["/mplex"]);
const stream = await mss.handle(connection, ["/my-custom-protocol"]);
await stream.upgrade(["/custom-auth"]);
// Use stream
Of course this is a little verbose and defaults could be set to make this easier but this would grant a lot of flexibility to the protocol and allow the user pick and choose modules and stack them based on what they need. I could also see that this might help with more difficult transports like UDP or Sneakernet.
All this is in the context of the JavaScript libp2p library but from the specification it looks like this is the standard for the rest as well.
As a side note this sort of behavior can be layered on to virtual streams in the current JS implementation but suffers a little bit of inefficiency from doubling the multiplexing. For example:
const createCompressionDuplex = duplex => ({
sink: source => pipe(source, deflate, duplex),
source: pipe(duplex, enflate)
});
server.handle("/compression", ({ stream }) => {
// Create a duplex streaming iterable that transforms the intput and output.
const processedStream = createCompressionDuplex(stream);
mss.handle(processedStream, ["/echo"]).then(({ stream }) => {
return pipe(stream, stream);
});
});
client.dialProtocol(peerId, "/compression").then(stream => {
const processedStream = createCompressionDuplex(stream);
return mss.select(customStream, ["/echo"])
}).then(stream => {
// Use the /echo protocol over the /compression protocol.
});