SofaBuffers TypeScript - v0.11.0
    Preparing search index...

    Class OStream

    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 accumulator growingOStream builds — a helper over the streaming mode, not a mode of this class.

    Implements

    • ByteSink
    Index
    • 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.

      Parameters

      • buffer: Uint8Array
      • offset: number = 0
      • Optionalflush: FlushSink

      Returns OStream

    • 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.

      Returns Uint8Array

    • 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.

      Returns void

    • 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.

      Returns void

    • 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.

      Parameters

      • buffer: Uint8Array
      • offset: number = 0
      • carried: number = 0

      Returns void

    • Write a blob (arbitrary bytes) field.

      Parameters

      • id: number
      • data: Uint8Array

      Returns void

    • Write a boolean field (encoded as the unsigned value 0 or 1).

      Parameters

      • id: number
      • value: boolean

      Returns void

    • 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):

      • subtypes 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.

      Parameters

      Returns void

    • Write an IEEE-754 32-bit float field.

      Parameters

      • id: number
      • value: number

      Returns void

    • Write an array of IEEE-754 32-bit floats.

      Parameters

      • id: number
      • values: ArrayLike<number>

      Returns void

    • 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.

      Parameters

      • id: number
      • payload: Uint8Array

      Returns void

    • 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".

      Parameters

      • id: number
      • bits: number

      Returns void

    • Write an IEEE-754 64-bit double field.

      Parameters

      • id: number
      • value: number

      Returns void

    • Write an array of IEEE-754 64-bit doubles.

      Parameters

      • id: number
      • values: ArrayLike<number>

      Returns void

    • 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.

      Parameters

      • id: number

      Returns void

    • 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.

      Returns void

    • 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:

      • a wrapper-array element (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;
      • an array field already known to differ from a non-empty declared 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.

      Returns void

    • Write a signed integer field (zig-zag encoded).

      Parameters

      • id: number
      • value: number | bigint

      Returns void

    • Write an array of signed integers (each zig-zag + varint). See writeUnsignedArray.

      Parameters

      • id: number
      • values: ArrayLike<number | bigint>

      Returns void

    • Write a signed 64-bit array (zig-zag) from Long[] — the bigint-free path. Zig-zag (n << 1) ^ (n >> 63) is computed on the lo/hi pair.

      Parameters

      • id: number
      • values: readonly Long[]

      Returns void

    • 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.

      Parameters

      • id: number
      • value: Long

      Returns void

    • Write a UTF-8 string field.

      Parameters

      • id: number
      • text: string

      Returns void

    • Write an unsigned integer field.

      Parameters

      • id: number
      • value: number | bigint

      Returns void

    • 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:

      • block mode (no sink) — the buffer is meant to hold the whole message, so the kernel always runs and the buffer's length is the bound, checked after the fact. A message that does not fit is BUFFER_FULL, which is precisely this mode's answer.
      • a stream with room — 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.
      • a chunk too small for that — fill it to the last byte, hand it over, carry on, splitting an element where it falls. That is not a fallback but the mode's contract: 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.

      Parameters

      • id: number
      • values: ArrayLike<number | bigint>

      Returns void

    • 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.

      Parameters

      • id: number
      • values: readonly Long[]

      Returns void

    • 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.

      Parameters

      • id: number
      • value: Long

      Returns void