flatbuffers

js/format/flatbuffers.ts

fino:format/flatbuffers - schema-less FlatBuffers reading and writing.

FlatBuffers is a zero-copy binary serialization format: tables store fields through a vtable indirection so readers can access individual fields without unpacking, and all scalars are little-endian at fixed offsets. This module implements the wire format directly — no schema compiler or generated code. Callers address table fields by their schema field id (the id attribute in a .fbs file, or declaration order starting at 0).

The reader (FlatBuffer, Table, Vector) validates bounds on every access and throws FlatbufferError with a hex-dump diagnostic for malformed buffers. The writer (Builder) implements the standard back-to-front construction algorithm with vtable deduplication, producing buffers byte-compatible with the reference implementation.

import { Builder, FlatBuffer } from 'fino:format/flatbuffers';

const b = new Builder();
const name = b.createString('fino');
b.startTable(2);
b.addFieldOffset(0, name);
b.addFieldInt32(1, 42, 0);
const root = b.endTable();
b.finish(root);

const table = FlatBuffer.from(b.bytes()).rootTable();
table.string(0); // 'fino'
table.i32(1, 0); // 42

Useful references: - FlatBuffers internals: https://flatbuffers.dev/internals/ - Format specification: https://flatbuffers.dev/formats/

Classes

class FlatbufferError extends ParseError {

Error thrown when a FlatBuffer is malformed or an access runs out of bounds.

FlatbufferError extends ParseError with a binary source, so render() produces a hex dump around the failing offset.

import { FlatBuffer, FlatbufferError } from 'fino:format/flatbuffers';

try {
  FlatBuffer.from(new Uint8Array([1, 2])).rootTable();
} catch (error) {
  if (error instanceof FlatbufferError) console.error(error.render());
}

class FlatBuffer {

A read-only FlatBuffer with bounds-checked positional accessors.

Wraps a byte buffer and resolves the root table. Positional readers (u8At, i32At, f64At, ...) are used for inline structs, whose layout only the caller knows.

import { FlatBuffer } from 'fino:format/flatbuffers';

// wireBytes: a Uint8Array received over the network or read from disk
const fb = FlatBuffer.from(wireBytes, { sizePrefixed: true });
if (!fb.hasIdentifier('MONS')) throw new Error('not a Monster buffer');
const root = fb.rootTable();

// Inline structs have no self-describing layout; read their members
// with the positional accessors from the struct's absolute position.
const posOffset = root.struct(0);
if (posOffset !== null) {
  const x = fb.f32At(posOffset);
  const y = fb.f32At(posOffset + 4);
}

Static Methods

static from(bytes: Uint8Array | ArrayBuffer, options?: { sizePrefixed?: boolean; }): FlatBuffer

Wrap bytes as a FlatBuffer. Pass sizePrefixed: true when the buffer begins with the standard 4-byte length prefix (finish(..., { sizePrefixed: true }) output); the prefix is validated and stripped.

Constructors

constructor(bytes: Uint8Array)

Construct directly over bytes with no size prefix. Prefer FlatBuffer.from().

Getters

get bytes(): Uint8Array

The wrapped bytes (a view, not a copy).

Methods

u8At(pos: number): number

Read an unsigned 8-bit integer at pos.

i8At(pos: number): number

Read a signed 8-bit integer at pos.

u16At(pos: number): number

Read an unsigned 16-bit integer at pos.

i16At(pos: number): number

Read a signed 16-bit integer at pos.

u32At(pos: number): number

Read an unsigned 32-bit integer at pos.

i32At(pos: number): number

Read a signed 32-bit integer at pos.

u64At(pos: number): bigint

Read an unsigned 64-bit integer at pos as a bigint.

i64At(pos: number): bigint

Read a signed 64-bit integer at pos as a bigint.

f32At(pos: number): number

Read a 32-bit float at pos.

f64At(pos: number): number

Read a 64-bit float at pos.

rootTable(): Table

Resolve the root table of the buffer.

tableAt(pos: number): Table

Wrap the table at absolute position pos, validating its vtable.

identifier(): string | null

The 4-character file identifier stored after the root offset, or null when the buffer is too short to carry one. FlatBuffers has no in-band flag for identifiers, so a buffer without one returns 4 arbitrary bytes — compare with hasIdentifier() instead of trusting this value.

hasIdentifier(id: string): boolean

Whether the buffer carries the given 4-character file identifier.

Throws a TypeError if id is not exactly 4 characters.

class Table {

A table within a FlatBuffer, with field accessors keyed by schema field id. Missing fields return the supplied default (scalars) or null (offsets), exactly like generated FlatBuffers accessors.

Unions follow the standard two-field convention: the type tag is a u8 field at id N and the value is a table field at id N + 1 — read them with u8(N, 0) and table(N + 1).

import { FlatBuffer } from 'fino:format/flatbuffers';

// Schema: table Monster { name: string; hp: short = 100; friend: Monster; }
const monster = FlatBuffer.from(wireBytes).rootTable();
monster.string(0);          // name, or null when absent
monster.i16(1, 100);        // hp, falling back to the schema default
monster.table(2)?.string(0); // friend's name, if a friend is set

Getters

get position(): number

Absolute position of this table in the buffer.

Methods

fieldPos(id: number): number

Absolute position of field id's inline data, or 0 when the field is absent. This is the raw vtable lookup all typed accessors build on.

bool(id: number, defaultValue: boolean): boolean

Read a boolean field.

u8(id: number, defaultValue: number): number

Read an unsigned 8-bit field.

i8(id: number, defaultValue: number): number

Read a signed 8-bit field.

u16(id: number, defaultValue: number): number

Read an unsigned 16-bit field.

i16(id: number, defaultValue: number): number

Read a signed 16-bit field.

u32(id: number, defaultValue: number): number

Read an unsigned 32-bit field.

i32(id: number, defaultValue: number): number

Read a signed 32-bit field.

u64(id: number, defaultValue: bigint): bigint

Read an unsigned 64-bit field as a bigint.

i64(id: number, defaultValue: bigint): bigint

Read a signed 64-bit field as a bigint.

f32(id: number, defaultValue: number): number

Read a 32-bit float field.

f64(id: number, defaultValue: number): number

Read a 64-bit float field.

string(id: number): string | null

Read a string field, or null when absent.

table(id: number): Table | null

Read a nested-table field, or null when absent.

struct(id: number): number | null

Absolute position of an inline struct field, or null when absent. Read the struct's members with the buffer's positional accessors (fb.f32At(pos + 4), ...).

vector(id: number): Vector | null

Read a vector field, or null when absent.

class Vector {

A vector within a FlatBuffer. Element accessors are typed by the caller (the wire format does not carry element types); indexes are bounds-checked.

import { FlatBuffer } from 'fino:format/flatbuffers';

// Schema: table Monster { ...; inventory: [ubyte]; weapons: [Weapon]; }
const monster = FlatBuffer.from(wireBytes).rootTable();

const inventory = monster.vector(3);
inventory?.bytes(); // zero-copy Uint8Array view of the [ubyte] contents

const weapons = monster.vector(4);
for (let i = 0; i < (weapons?.length ?? 0); i++) {
  const weapon = weapons!.table(i);
  console.log(weapon.string(0), weapon.i16(1, 0));
}

Getters

get length(): number

Number of elements.

get buffer(): FlatBuffer

The owning FlatBuffer, for reading inline-struct members at absolute positions returned by structAt.

Methods

elemPos(i: number, elemSize: number): number

Absolute position of element i, given the element byte size.

bool(i: number): boolean

Read element i as a boolean.

u8(i: number): number

Read element i as an unsigned 8-bit integer.

i8(i: number): number

Read element i as a signed 8-bit integer.

u16(i: number): number

Read element i as an unsigned 16-bit integer.

i16(i: number): number

Read element i as a signed 16-bit integer.

u32(i: number): number

Read element i as an unsigned 32-bit integer.

i32(i: number): number

Read element i as a signed 32-bit integer.

u64(i: number): bigint

Read element i as an unsigned 64-bit bigint.

i64(i: number): bigint

Read element i as a signed 64-bit bigint.

f32(i: number): number

Read element i as a 32-bit float.

f64(i: number): number

Read element i as a 64-bit float.

string(i: number): string

Read element i as a string (vector of string offsets).

table(i: number): Table

Read element i as a table (vector of table offsets).

structAt(i: number, structSize: number): number

Absolute position of inline-struct element i, given the struct's byte size.

bytes(elemSize = 1): Uint8Array

A raw byte view over all elements (no copy), given the element byte size. For a [ubyte] vector this is the vector's contents directly.

class Builder {

FlatBuffers builder implementing the standard back-to-front construction algorithm with vtable deduplication.

Usage follows the reference Builder: create strings/vectors/nested tables first (bottom-up), then startTable(fieldCount), add fields by id, endTable(), and finish(root).

import { Builder } from 'fino:format/flatbuffers';

const b = new Builder();
const s = b.createString('hi');
b.startTable(1);
b.addFieldOffset(0, s);
b.finish(b.endTable());
const bytes = b.bytes();

Constructors

constructor(initialSize = 1024)

Create a builder with an optional initial capacity (bytes).

Methods

forceDefaults(value: boolean): void

When true, scalar fields equal to their default are still written. Defaults to false (matching the reference Builder).

offset(): number

Current builder offset (bytes written so far). Offsets returned by createString/endTable/endVector are in this space.

pad(n: number): void

Write n zero padding bytes.

prep(size: number, additionalBytes: number): void

Prepare to write additionalBytes after aligning to size. Grows the buffer as needed. Public so callers can build inline structs with the raw write* methods.

writeInt8(value: number): void

Write a raw signed 8-bit value (no alignment).

writeInt16(value: number): void

Write a raw signed 16-bit value (no alignment).

writeInt32(value: number): void

Write a raw signed 32-bit value (no alignment).

writeInt64(value: bigint): void

Write a raw signed 64-bit value (no alignment).

writeFloat32(value: number): void

Write a raw 32-bit float (no alignment).

writeFloat64(value: number): void

Write a raw 64-bit float (no alignment).

addInt8(value: number): void

Align and write a signed 8-bit value.

addInt16(value: number): void

Align and write a signed 16-bit value.

addInt32(value: number): void

Align and write a signed 32-bit value.

addInt64(value: bigint): void

Align and write a signed 64-bit value.

addFloat32(value: number): void

Align and write a 32-bit float.

addFloat64(value: number): void

Align and write a 64-bit float.

addOffset(offset: number): void

Align and write a relative offset to a previously written object.

addFieldBool(id: number, value: boolean, defaultValue: boolean): void

Add a boolean field to the current table.

addFieldInt8(id: number, value: number, defaultValue: number): void

Add an 8-bit scalar field to the current table.

addFieldInt16(id: number, value: number, defaultValue: number): void

Add a 16-bit scalar field to the current table.

addFieldInt32(id: number, value: number, defaultValue: number): void

Add a 32-bit scalar field to the current table.

addFieldInt64(id: number, value: bigint, defaultValue: bigint): void

Add a 64-bit scalar field to the current table.

addFieldFloat32(id: number, value: number, defaultValue: number): void

Add a 32-bit float field to the current table.

addFieldFloat64(id: number, value: number, defaultValue: number): void

Add a 64-bit float field to the current table.

addFieldOffset(id: number, offset: number): void

Add an offset field (string, vector, or nested table) to the current table. Zero offsets (absent) are skipped.

addFieldStruct(id: number, offset: number): void

Add an inline struct field to the current table. The struct must have been written immediately before this call (its offset must equal the current builder offset); otherwise this throws.

Structs are written with prep() plus the raw write* methods, members in reverse declaration order (the builder writes back-to-front):

import { Builder } from 'fino:format/flatbuffers';

// struct Vec3 { x: float; y: float; z: float; } — 12 bytes, align 4
const b = new Builder();
b.startTable(1);
b.prep(4, 12);
b.writeFloat32(z);
b.writeFloat32(y);
b.writeFloat32(x);
b.addFieldStruct(0, b.offset());
b.finish(b.endTable());
startTable(numFields: number): void

Begin a table with numFields field slots.

Throws if another table or vector is already being built — tables do not nest during construction; build inner objects first and reference them by offset.

endTable(): number

Finish the current table, writing (or reusing) its vtable. Returns the table's builder offset.

Throws if no table is being built.

startVector(elemSize: number, numElems: number, alignment: number): void

Begin a vector of numElems elements of elemSize bytes, aligned to alignment. Write elements back-to-front with the raw write* methods or addOffset, then call endVector().

Because the builder writes toward lower addresses, the last element is written first. Throws if a table or vector is already being built.

import { Builder } from 'fino:format/flatbuffers';

// Build [10, 20, 30] as an [int] vector.
const b = new Builder();
b.startVector(4, 3, 4);
b.writeInt32(30);
b.writeInt32(20);
b.writeInt32(10);
const vec = b.endVector();
b.startTable(1);
b.addFieldOffset(0, vec);
b.finish(b.endTable());
endVector(): number

Finish the current vector and return its builder offset.

Throws if no vector is being built.

createString(value: string): number

Write a UTF-8 string (with NUL terminator, per the format) and return its builder offset.

createByteVector(bytes: Uint8Array): number

Write a [ubyte] vector from raw bytes and return its builder offset.

finish(rootTable: number, options?: { fileIdentifier?: string; sizePrefixed?: boolean; }): void

Finalize the buffer with rootTable as the root. An optional 4-character fileIdentifier is stored after the root offset; sizePrefixed writes the standard 4-byte length prefix.

Throws a TypeError if fileIdentifier is not exactly 4 characters.

bytes(): Uint8Array

The finished buffer contents (a view over the builder's storage, not a copy). Throws if called before finish().