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().