Encoder over a caller buffer, optionally draining to flush as it fills.
Those are the only two buffer models there are (CORELIB_PLAN §5.1.1): the buffer the caller hands over, and whatever a sink installs in its place (§5.1.5). There is no third parameter through which the encoder could be given something to enlarge — growing a destination from a write path is what §6.6's second violation row names, whoever owns the allocator.
Constructing one is the only allocating step (§6.6): it sizes the hold-back
run from MAX_DEPTH and reads the active kernel once. No write* call after
that allocates anything.
Optionalflush: FlushSinkBytes of the message currently held in the buffer (see bytes).
The encoded message so far, as a view into the working buffer: everything
written since construction, the last reset, or the last flush the
sink returned from without installing a buffer. With a flush sink that is
normally only the not-yet-flushed tail; on a stream whose sink hands back a
replacement carrying the earlier bytes — the accumulator growingOStream
builds — it is the whole message. The view is valid until the next write.
Drain buffered bytes to the flush sink (no-op without one).
A sink that returns without installing a buffer has copied what it was
handed, so the encoder keeps writing into the same buffer — resuming at
offset 0. The start offset belongs to the installation, not to the
buffer (CORELIB_PLAN §5.1): the buffer-set that armed it — the constructor
or OStream.setBuffer — reserved room in the unit it began, and
handing that unit over consumes the reservation. A sink that wants header
room in every unit re-arms it by calling setBuffer(buf, offset) from
inside the callback, a new installation like any other; a bare return must
not do it implicitly, or the leading bytes would be capacity the rest of the
stream could never use and the two shapes would be indistinguishable.
Rewind the encoder to empty, reusing the existing buffer. Lets a caller pool one OStream across many messages instead of allocating a fresh buffer per encode. Any view previously returned by bytes is invalidated.
Install a fresh output buffer to write into, mid-stream. Intended for the
streaming (flush-sink) mode: call it from inside your flush callback to hand
the encoder a new buffer for the next batch of bytes, so encoding continues
without interruption. offset reserves space at the front of the new
buffer. Any not-yet-flushed bytes in the old buffer are dropped, so
flush first (the flush callback fires before you swap).
Every call is a new installation, and its offset applies to the unit
it begins and is consumed when that unit is flushed (CORELIB_PLAN §5.1).
Passing the buffer the encoder already has is an installation like any
other: that is how a sink gets header room in every flushed unit — one
framing header per packet — where returning bare would resume at 0.
On a stream that has a flush sink the new buffer must leave at least
MIN_OUTPUT_BUFFER usable bytes (buffer.length - offset); a smaller
one is rejected here, with SofabErrorCode.Argument, leaving the
encoder on the buffer it already had. A sink-less stream has no minimum.
carried says how many bytes immediately before offset the
replacement already holds of this message — normally 0, because a fresh
buffer holds none. A caller that keeps the whole message in one growing
store passes the length it copied across, so bytes keeps reporting
the message rather than only the piece written after the swap. It changes
nothing on the wire and nothing about flushing: the next flush still begins
at offset. growingOStream is the caller this exists for; it is the
§5.1.5 handover, not a growth hook inside the encoder (§6.6).
The accumulating stream growingOStream builds is an ordinary streaming
stream, so this works on it too, and means exactly what it means anywhere
else: the not-yet-flushed bytes in the old buffer are dropped, encoding
continues into yours, and its sink takes over growing that one from the
next flush — reserve offset included, since what it copies out is the
message rather than the buffer. Encode into a plain
new OStream(buffer, offset, flush?) to keep the buffer yours instead.
Write a blob (arbitrary bytes) field.
Write a boolean field (encoded as the unsigned value 0 or 1).
Write a fixed-length field of the given subtype from raw bytes.
This is the byte-level entry point — the one writer that takes the subtype
from the caller rather than picking it — so the payload is checked
against that subtype before a byte is written, and it cannot emit a
fixlen_word a conformant decoder must reject (ARGUMENT, §6.3):
0x4–0x7 are reserved — a decoder must treat a field
carrying one as malformed (INVALID, §4.6/§5.2);Fp32 / Fp64 payloads are exactly 4 / 8 bytes — any other declared
length for those subtypes is malformed, rejected the moment the word is
read (§4.6);String / Blob take any length up to FIXLEN_MAX.The typed writers (writeFp32, writeFp64, writeString) are correct by construction and go straight to the header; only writeBlob, whose subtype is unconstrained anyway, shares this path.
Write an IEEE-754 32-bit float field.
Write an array of IEEE-754 32-bit floats.
Write an fp32 array from its raw little-endian element payload. The bytes
are emitted verbatim — no per-element setFloat32 — so a signaling NaN
survives bit-for-bit (§4.6), which writeFp32Array cannot guarantee
because it re-quantizes each JS number. payload.length must be a
multiple of 4; the element count is payload.length / 4.
Write an fp32 field from its raw wire bits — the 4 little-endian payload
bytes as one 32-bit word, which is exactly what Visitor.fp32 delivers
as bits.
This is the re-encode half of the bit-exactness rule (CORELIB_PLAN §6.5). A JS
number is a 64-bit double, and widening an fp32 signaling NaN into one
quiets it, so re-encoding through writeFp32 cannot reproduce such a
payload; the bits go out verbatim here, so decode → re-encode is byte-for-byte
for every fp32 value, sNaN included. §6.5 requires this path of every
double-only target, and names it: "a 32-bit bits accessor".
Write an IEEE-754 64-bit double field.
Write an array of IEEE-754 64-bit doubles.
Open a nested sequence (a fresh id scope) whose header is held back until the sequence turns out to have content.
MESSAGE_SPEC §2 omits a sequence-typed field whose value equals its declared default, and "not one child was written" is exactly that condition — evaluated per child field, recursively, for free, because the message layer already omits every child equal to its default. A sequence closed with nothing in it therefore emits nothing instead of a two-byte empty frame, and an all-default message becomes the empty byte string. No byte image is ever compared, so in-memory layout never enters the decision.
This is the only way to open a sequence. How it closes decides whether a contentless one survives: OStream.writeSequenceEnd drops it, OStream.writeSequenceEndKeep forces the frame out.
Close the current sequence, letting it vanish if it received no content.
Use it wherever absence encodes the same value as an empty frame: a
struct/union field, and an array field whose declared default is the
empty collection (MESSAGE_SPEC §2). Where the frame must be visible, close
with OStream.writeSequenceEndKeep instead.
An end with no matching begin is not rejected: the encoder writes what it is told, and the resulting bytes are then malformed, which is the decoder's verdict to make. No other port refuses it. The depth counter stops at zero so the MAX_DEPTH check on begin cannot be fooled by an underflow.
Close the current sequence, keeping its frame even when it received no content.
Behaves like a write: it first emits any held-back headers — this frame's
and every enclosing one's — and then the end marker, so an empty sequence
reaches the wire as begin + end.
Required wherever the frame carries information beyond its contents:
struct/union/nested row): element
presence is what carries a dynamic array's length — highest present id +
1 (MESSAGE_SPEC §5.1) — so dropping an all-default element would change
the decoded length, not just the bytes;default: absence would reconstruct that default, so the empty frame is
the only encoding of "explicitly empty" (§2, §3).The two failure directions are not symmetric, which is why this is the safe choice when in doubt: using it where OStream.writeSequenceEnd would do costs one non-canonical empty frame that a decoder normalizes away, while the reverse silently changes an array's length.
Write a signed integer field (zig-zag encoded).
Write an array of signed integers (each zig-zag + varint). See writeUnsignedArray.
Write a signed 64-bit scalar (zig-zag) from a Long — the
bigint-free twin of writeSigned, and the scalar counterpart of
writeSignedArrayLong. Zig-zag (n << 1) ^ (n >> 63) is computed on
the lo/hi pair, so the varint goes out at its exact size (a fixed caller
buffer must not see a 10-byte demand for a 2-byte field) and no bigint is
created. A Long carries exactly 64 bits, so as in writeUnsignedLong
there is nothing left to range-check.
Write a UTF-8 string field.
Write an unsigned integer field.
Write an array of unsigned integers (each a varint).
The bulk kernel writes a whole array in one pass and cannot flush, so it runs only where everything is known to fit. Three cases, and none of them asks the source how wide its elements are:
BUFFER_FULL, which is
precisely this mode's answer.VARINT_MAX_BYTES per element is the true
worst case for any 64-bit value, and a growing stream always satisfies
it because it grows to whatever is asked.MIN_OUTPUT_BUFFER is 1.What is gone is the fourth case, which asked values.constructor for a
narrower bound so the kernel would run on a tightly-sized chunk. constructor
is an ordinary property, an ArrayLike can claim any width, and a wrong
answer silently truncated the message — §5.1's "partial output handed back as
complete". The block mode now reaches the kernel without needing the number
at all, which is where a tightly-sized buffer actually lives.
Write an unsigned 64-bit array from Long[] — the bigint-free path.
Produces the identical wire to writeUnsignedArray; reads each Long's
32-bit halves directly, so no bigint is created per element.
Write an unsigned 64-bit scalar from a Long — the bigint-free twin
of writeUnsigned, and the scalar counterpart of
writeUnsignedArrayLong. Produces the identical wire.
There is no range check and no scratch round-trip: a Long is two 32-bit
halves, so it is in the uint64 domain by construction — which is the whole
of what splitU64 decides for a number | bigint. The halves go straight
into the varint writer, so nothing is allocated per value. Nothing needs
copying out ahead of header either, for the same reason the array writers
do not: the halves come off a caller-owned immutable Long, not the shared
scratch a re-entrant flush sink could overwrite.
Encoder for the SofaBuffers wire format. Each
write*method appends one field and maps one-to-one onto a wire type. It writes into the buffer the caller supplies and into no other: it allocates none of its own, grows none it was given, and has no hook through which anything else could grow one for it (CORELIB_PLAN §5.1.2, §6.6). Hand it a buffer that holds the whole message, or one that drains to a FlushSink as it fills so the message can outgrow it. Invalid arguments and a full buffer with no sink throw SofabError. To let the buffer follow the message instead, encode into the accumulatorgrowingOStreambuilds — a helper over the streaming mode, not a mode of this class.