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 either a single-byte discriminator (the preserved opcode is one byte, re-encoded verbatim) or a varint discriminator — a value class whose inner scalar is
@UseCodec(<VariableLengthCodec>) raw: Long | ULong(QUIC varint, LEB128, …). A varint opcode is preserved as its full decoded value and re-encoded through the discriminator's own codec, so a multi-byte GREASE-style type round-trips in its canonical minimal encoding. Fixed multi-byte discriminators (UShort/UInt) are not supported.unknown must name a member of the sealed type marked UnknownVariant, whose primary constructor is shaped
(opcode: Int, raw: PlatformBuffer)(aReadBuffer-typedrawis also accepted). For a single-byte discriminatoropcodeisIntand carries the discriminator byte; for a varint discriminator it must be the discriminator's own inner type (Long/ULong) and carries the full decoded type value.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.