Sadly there is not: all the documentation we have is the Jazzy-generated API documentation and the README. I'd like to have more narrative documentation but I haven't familiarised myself with the way Jazzy handles narrative documentation currently.
Yup, that's the limitation, and I definitely understand the concern there.
In some ways you can think of ByteBuffer as conceptually like Data plus a pair of indices offsetting into it. This is not quite right (ByteBuffer handles uninitialised memory differently) but it's a good enough abstraction to get by. This means you can probably make Data fit ByteBuffer's interface by wrapping it in a structure that holds a reader and writer index into the underlying Data, and then shimming together the APIs you need.
I'd have argued this mentions the .continue state by way of omission: if the method will be called until either the ByteBuffer has nothing to read left or until .needMoreData is returned, then presumably returning .continue will lead to this method continuing to be called. That said, it could definitely be called out more clearly.
As to whether the readableBytes count affects whether the decode method will be called, yes it does. I think I mentioned this above but I'll restate: returning .continue is a signal to the decoder that you haven't checked if there are more bytes available in the buffer, but if there are you believe you might be able to make forward progress. If there are no bytes in the buffer then there is no point in calling decode, so NIO won't.
Yes, that's right. decode is not a coroutine, and so we can only ever call it again.
This ambiguity seems to be to derive from the fact that the NIO developers have a mental model of how decode is called that does not match to yours. This is useful to know because it means we can clean the documentation up to clarify. In this case, I think the clarification that is needed is that decode is called in a loop until either .needMoreData is returned or the ByteBuffer has .readableBytes == 0. That's what is meant by "continue calling this method". But if you're not 100% confident of that looping behaviour, I can definitely see how the sentence would be ambiguous.
Mu. 
The values of the reader and writer index of the ByteBuffer are not part of the observable state of the ByteToMessageHandler outside of a call to decode. They are valid and respected throughout that call, and after that call their values can and do change. The only guarantee made about the buffer is that any byte that is "readable" (that is, the byte is in the section of the buffer between the readerIndex and the writerIndex) will be in the buffer the next time decode is called, and will be in the same place relative to the readerIndex.
In practice, under the hood the ByteToMessageHandler will reallocate and potentially shrink the storage buffer as it sees fit in order to avoid wasting memory, so you absolutely mustn't rely on readerIndex having the same numerical value between two calls to decode.
However:
Yes, you can see multiple messages in the buffer. This is the result of ByteToMessageHandler naturally operating on streams of data. Keeping the boundaries of the data returned from individual socket reads is a non-goal, because those boundaries are non-semantic: they just happen to reflect the state of the socket operation. The data has come from the network as a stream, and your code should think of it that way.
decodeLast is a "bring out your dead" method. It's called when we know that no further data will ever be received by this ChannelHandler. This can happen either because the network connection has been closed, or because your ByteToMessageDecoder is being removed from the pipeline.
We notify you of this in this way for the following reasons:
- In some protocols, reading EOF is semantic. This happens in HTTP for example: some messages are delimited by connection closure. In this case, you may want to feed EOF to your parser implementation.
- You may have some bytes leftover and need to decide what to do with them. The
HTTPRequestDecoder is a good example here, as it has a RemoveAfterUpgradeStrategy enum that controls the things it will do with excess data in decodeLast: it can fire them in an error, fire them as bytes, or drop them. This is basically your list of options too.
It's not contradictory, it will be called again. Once more, decodeLast is called in a loop until either of these conditions are met.
No. seenEOF == false means that your handler is being removed. It's useful only as a flag for the cases where EOF is semantic for your protocol: if it isn't, you can safely ignore it.
Here are some notes.
- You're using a
Data(ByteBuffer.getByes()) construction quite a bit. Are you intentionally copying the bytes out? You can avoid the copy by importing NIOFoundationCompat and using ByteBuffer.getData() instead.
- The first line (that wants to call
Data.parseRemainingLength) is pretty inefficient: it creates a whole new Data object containing all the bytes from the ByteBuffer. Is it possible to implement parseRemainingLength on top of Collection? If it is, you can use ByteBuffer.readableBytesView to get a Collection without copying.
- Your
guard has a line that does let _ = buffer.readSlice(length: Int(count)). I assume you're doing this to allow you to write the guard let, but if you ever need only the side effect of the read method you can just call moveReaderIndex(forwardBy:).
- Instead of using
rest.getBytes() (or rest.getData() if you migrate to that), why not use read.readBytes()? It requires you do guard var instead of guard let, but it'll avoid the need to specify where you're reading from. In general read is better than get when you can safely use it (that is, when you're consuming bytes).
Otherwise this looks very good! I've also opened an issue to improve the ByteToMessageDecoder docs.