Light's Replication Format
Notation
Some shorthand should be established before reading:
-
<D>:size(<Value?>)
The size of a datatype given a value. -
<D>[<L>]
: An array containing items represented byD
datatype, with the number of items represented as datatypeL
. -
{<K>:<V>}
: A map of keys which are represented byK
datatype, and values represented byV
datatype. -
vlq<N>
: A variable length quantity ofN
maximum bytes. Ex. vlq2, vlq3, etc.
General Encoding
Bitpack
To optimize bandwidth, sometimes light needs to bitpack multiple booleans into a single byte. The
implementation for a bitpack is generally outside of the scope of this document, but it's important to understand that
they are encoded as any number of bytes, notated as byte[<L>]
or byte
. Technically, byte
is the same as u8
, but
byte
and byte[<L>]
will be reserved specifically for bitpacks to avoid confusion. Bitpacked booleans are annotated
as bit
, and an optional value as opt(T)
.
Boundary Array
An array of u8s signifying a number of identical booleans in sequence notated as bbyte
or bbyte[<L>]
. Light streams
encode a "state" for bbyte
s, defaulting to false. When encountering a boolean that does not match the current state of
the bbyte
, the state will "flip", and write the number of items from the previous state into the bbyte
. I.e., take
the following list of booleans:
Optional values encoded in the bbyte
are referred to as bopt
s, and booleans as bbit
. Boundary arrays, bopt
s, and
bbit
s are currently unavailable to users directly. For now, they are only used for internal encoding.
Message Encoding
A message can be "high" or "low". The first 256 messages you define in light are "low", and thus use a u8
in the
replication step. Messages defined after that are encoded with a u16
. Messages generally store multiple fires in a
"Content Array", rather than encoding the message ID for every message fire. To give users access to bitpack
optimizations, each message also has a bopt(byte[vlq2])
access to bitpacked opts
in the form of message bitpacks. A message more or less looks like this in the stream:
u8|u16
The message's ID. "Low" IDs are annotated asLoMsg
, and "High" IDs are annotated asHiMsg
.bopt(byte[vlq2])
To give users access to optimizations involving packed bits, each message ID has an optionalbyte[vlq2]
bitpack. Since a message might not have any bitpack data at all, thebopt
for this component of the message will be false if the message's bitpack is empty.vlq4
The number of bytes of content.Content[]
All of the data encoded to this message since it was last exported.
Stream Format
Here the encoding specifications for each part of the replication step are defined in order.
Global Bitpack (byte
)
A single byte encoding up to 8 opt
s or bit
s for light's entire internal replication step encoding schema. Things
that contribute to this limit of 8 will be annotated as either gopt
s or gbit
s.
Global Boundary Array (gopt(bbyte[vlq2])
)
Messages use a bopt
for creating optional message-specific bitpacks, those bopt
s are encoded here. If the boundary
array is empty (a.k.a. all bbit
s & bopt
s are false), the outer gopt
will be false and no boundary array will be
encoded for this stream.
Message Arrays
Generally, there are two places you can find Messages in the replication step:
- Low (
gopt(LoMsg[vlq2])
) - High (
gopt(HiMsg[vlq2])
)
Low and High message arrays are lists of all the u8
messages and u16
messages respectively. Both of these arrays are
technically optional, but if neither low nor high messages have been sent this step, no replication batch will be
created at all.
Export Stream
Code is slightly abridged for simplicity and readability. Stream exporting can be abstracted into 3 steps:
-
Initialization
Initialize variables, values, etc. to use for encoding.
-
Allocation
Because reallocating an entire stream is expensive, all allocation for the final stream buffer is done ahead of time. Once allocation is done, the next step, Writing, will copy and write relevant data into the outgoing buffer.
-
Writing
Creates a buffer with the allocated size, and encodes all of the stream's data.
Initialization
-
local stream_pack = new_bitpack()
-
local boundarr = new_boundary_arr()
Construct the global boundary array. Wait until later to allocate.
-
local allocated = 0
Initialize an allocated number of bytes the stream takes up for the "Allocation" step to use.
-
Split Messages
Split up
LoMsg
andHiMsg
data into lo_batch and hi_batch respectively.
Allocation
-
allocated += 1 -- stream_pack
-
u8
Messages (gopt(LoMsg[vlq2])
) -
u16
Messages (gopt(HiMsg[vlq2])
) -
Boundary Array
Since all pushes to the bitpack bound array have been completed, it can now be allocated:
Writing
-
local send_buff = buffer.create(allocated)
-
local send_ptr = 0
-
local assert_not_reallocated: () -> ()
-
Stream Pack
All global bits have been compiled, now they can be written:
-
Boundary Array
-
Low Messages
-
High Messages