Forward Compatible
Marks a @ProtocolMessage sealed dispatch parent as forward compatible: a decoder that hits a discriminator it does not recognize skips the unknown variant's framed payload and preserves it verbatim into the unknown variant, instead of throwing DecodeException.
This lets newer protocol ops survive a round-trip through an older decoder (relay) or an on-disk frame (persistence): the bytes are read into an opaque buffer on decode and re-emitted byte-for-byte on encode.
Requirements (enforced at compile time)
The annotated type must also carry FramedBy — you cannot skip a variant whose length you cannot measure. The framing prefix bounds the payload the decoder skips.
The annotated type must use DispatchOn dispatch with a single-byte discriminator. (Framed sealed dispatch routes exclusively through
@DispatchOn; a single-byte discriminator guarantees the preserved opcode re-encodes byte-identically.)unknown must name a member of the sealed type marked UnknownVariant, whose primary constructor is shaped
(opcode: Int, raw: PlatformBuffer)(aReadBuffer-typedrawis also accepted).opcodecarries the discriminator byte (the only place it can survive —rawis the payload only);rawcarries the opaque framed payload, excluding the opcode and length prefix.
@ProtocolMessage
@DispatchOn(OpCode::class)
@FramedBy(OpLengthCodec::class, after = "header")
@ForwardCompatible(unknown = Op.Unknown::class)
sealed interface Op {
@ProtocolMessage @PacketType(value = 0x12, wire = 0x12)
data class Scroll(val header: OpCode, /* ... */) : Op
@UnknownVariant
data class Unknown(val opcode: Int, val raw: PlatformBuffer) : Op
}Preserved bytes are allocated through ForwardCompatibleFactoryKey (default BufferFactory.managed() — GC lifetime, no manual free). A caller wanting native/pooled memory injects a pool-backed factory via that context key and owns freeing.
Parameters
The UnknownVariant-marked member of the sealed type that receives skipped-and-preserved ops.