Core

Deserialization

Read binary data back into typed values using Sia's read methods, with error handling and zero-copy options.

Overview

Deserialization in Sia means reading typed values back from a byte buffer using read* methods. Each read* method:

  1. Checks that enough bytes remain in the buffer
  2. Reads the value at the current offset
  3. Advances the offset by the number of bytes consumed

Since Sia's binary format contains no field names or type markers, you must read values in the exact same order they were written. The read types must also match the write types: reading a UInt32 where a UInt16 was written will produce incorrect results.

Basic Reading

Create a Sia instance from existing bytes and read values in order:

import { Sia } from "@timeleap/sia";

// Assume `bytes` was produced by:
// sia.addString8("Alice").addUInt8(30).addBool(true)
const sia = new Sia(bytes);

const name = sia.readString8(); // "Alice"
const age = sia.readUInt8(); // 30
const active = sia.readBool(); // true

Or reuse the same instance by seeking back to the start:

const sia = new Sia();
sia.addUInt32(42).addString8("hello");

// Seek to the beginning to read back
sia.seek(0);
const value = sia.readUInt32(); // 42
const text = sia.readString8(); // "hello"

Seeking and Skipping

seek(offset): Absolute Position

Jump to a specific byte position in the buffer:

const sia = new Sia(bytes);

sia.seek(0); // go to the beginning
sia.seek(10); // jump to byte 10

skip(bytes): Relative Advance

Advance the offset by a given number of bytes:

const sia = new Sia(bytes);

// Skip past a UInt32 (4 bytes) without reading it
sia.skip(4);

// Read the next value
const name = sia.readString8();

Both methods return this for chaining:

const value = sia.seek(0).skip(4).readUInt16();

Common Use Case: Skip to a Known Field

If you know the byte layout, you can skip directly to the field you need:

// Layout: [version: UInt8][timestamp: UInt64][name: String8]
// To read just the name, skip the first 9 bytes (1 + 8)
sia.seek(0).skip(9);
const name = sia.readString8();

Reading with setContent

You can reuse a Sia instance by swapping its underlying buffer with setContent. This avoids creating new objects in hot loops:

const sia = new Sia();

for (const message of incomingMessages) {
  sia.setContent(message);
  // offset is automatically reset to 0

  const type = sia.readUInt8();
  const payload = sia.readString16();
  handleMessage(type, payload);
}

setContent replaces the internal content, size, dataView, and resets offset to 0.

Error Handling

Every read* method performs a bounds check before reading. If there isn't enough data remaining in the buffer, it throws an Error with a descriptive message:

const sia = new Sia(new Uint8Array(0));

try {
  sia.readUInt8();
} catch (e) {
  console.error(e.message); // "Not enough data to read uint8"
}

Error Messages by Type

MethodError Message
readUInt8"Not enough data to read uint8"
readInt8"Not enough data to read int8"
readUInt16"Not enough data to read uint16"
readInt16"Not enough data to read int16"
readUInt32"Not enough data to read uint32"
readInt32"Not enough data to read int32"
readUInt64"Not enough data to read uint64"
readInt64"Not enough data to read int64"
readByteArrayN"Not enough data to read byte array"

Every read* method checks bounds before accessing data, so you don't need to do it manually.

Reading Integers

All multi-byte integers are read in little-endian byte order, matching how they were written.

Unsigned
const u8 = sia.readUInt8(); // 1 byte
const u16 = sia.readUInt16(); // 2 bytes, little-endian
const u32 = sia.readUInt32(); // 4 bytes, little-endian
const u64 = sia.readUInt64(); // 8 bytes, little-endian
Signed
const i8 = sia.readInt8(); // 1 byte
const i16 = sia.readInt16(); // 2 bytes, little-endian
const i32 = sia.readInt32(); // 4 bytes, little-endian
const i64 = sia.readInt64(); // 8 bytes, little-endian

Implementation Detail

Here's how readUInt32 works internally, showing the bounds check and DataView read:

readUInt32(): number {
  if (this.offset + 4 > this.content.length) {
    throw new Error("Not enough data to read uint32");
  }
  const value = this.dataView.getUint32(this.offset, true);
  this.offset += 4;
  return value;
}

Reading Strings

String read methods decode the length prefix, read that many bytes, and decode them back to a JavaScript string:

UTF-8
const s8 = sia.readString8(); // 8-bit length prefix
const s16 = sia.readString16(); // 16-bit length prefix
const s32 = sia.readString32(); // 32-bit length prefix
const s64 = sia.readString64(); // 64-bit length prefix
ASCII
// Optimized ASCII decoder: faster than TextDecoder
const fixed = sia.readAsciiN(3); // no length prefix, caller specifies length
const ascii = sia.readAscii8(); // 8-bit length prefix
const ascii16 = sia.readAscii16(); // 16-bit length prefix
UTFZ
// Decompresses UTFZ-encoded string
const utfz = sia.readUtfz(); // 8-bit length prefix

Internally, readString8 reads a byte array and decodes it with TextDecoder:

readString8(): string {
  const bytes = this.readByteArray8(true);
  return this.decoder.decode(bytes);
}

Note that readString* methods pass asReference = true internally to avoid an unnecessary copy: the TextDecoder will produce a new string regardless.

Reading Byte Arrays

Byte array read methods support an optional asReference parameter that controls whether the returned Uint8Array is a copy or a view of the internal buffer.

Default (Copy)

const data = sia.readByteArray8(); // returns a new Uint8Array (copy)

The returned array is independent of the Sia buffer. Safe to store, modify, or pass to other APIs.

Zero-Copy Reference

const ref = sia.readByteArray8(true); // returns a subarray (view)

The returned array shares memory with the Sia buffer. Faster, but modifications to the buffer will affect the reference.

When using asReference = true, the returned Uint8Array is a view into the Sia buffer's underlying ArrayBuffer. If the buffer is reused (e.g., with setContent or by writing new data), the reference will see the new data. Only use references for temporary, immediate reads.

All Byte Array Variants

// With length prefix
const ba8 = sia.readByteArray8(); // 8-bit length prefix
const ba16 = sia.readByteArray16(); // 16-bit length prefix
const ba32 = sia.readByteArray32(); // 32-bit length prefix
const ba64 = sia.readByteArray64(); // 64-bit length prefix

// Fixed length, no prefix
const fixed = sia.readByteArrayN(16); // read exactly 16 bytes

// All accept asReference
const ref = sia.readByteArray32(true); // zero-copy

Reading Booleans and BigInts

Booleans

const flag = sia.readBool(); // true or false

Reads a single byte and returns true if the value is 1, false otherwise.

BigInts

const big = sia.readBigInt(); // bigint value

Reads an 8-bit length-prefixed byte array and converts it back to a bigint:

readBigInt(): bigint {
  const bytes = this.readByteArray8();
  let hex = "";
  bytes.forEach((byte) => {
    hex += byte.toString(16).padStart(2, "0");
  });
  return BigInt("0x" + hex);
}

Reading Arrays

Array read methods accept a deserializer function that is called once per element:

Simple Arrays
import { Sia } from "@timeleap/sia";

// Read an array of UInt16 values
const scores = sia.readArray8((s) => s.readUInt16());
// TypeScript infers: number[]
Object Arrays
interface Player {
  name: string;
  score: number;
  alive: boolean;
}

function readPlayer(sia: Sia): Player {
  return {
    name: sia.readString8(),
    score: sia.readUInt32(),
    alive: sia.readBool(),
  };
}

const players = sia.readArray8(readPlayer);
// TypeScript infers: Player[]
Nested Arrays
interface Team {
  name: string;
  members: string[];
}

function readTeam(sia: Sia): Team {
  return {
    name: sia.readString8(),
    members: sia.readArray8((s) => s.readString8()),
  };
}

const teams = sia.readArray16(readTeam);
// TypeScript infers: Team[]

Array Method Variants

MethodMax ItemsLength Prefix
readArray82551 byte
readArray1665,5352 bytes
readArray32~4 billion4 bytes
readArray642^53 - 18 bytes

The type parameter T is inferred from your deserializer function's return type: no explicit type annotation is needed.

Practical Example: Deserializing a Complete Message

Here's a complete example that pairs with a serializer to handle a structured message format:

import { Sia } from "@timeleap/sia";

interface Message {
  version: number;
  timestamp: number;
  sender: string;
  recipient: string;
  body: string;
  attachments: Uint8Array[];
  priority: number;
  encrypted: boolean;
}

function deserializeMessage(bytes: Uint8Array): Message {
  const sia = new Sia(bytes);

  return {
    version: sia.readUInt8(),
    timestamp: sia.readUInt64(),
    sender: sia.readString8(),
    recipient: sia.readString8(),
    body: sia.readString32(),
    attachments: sia.readArray8((s) => s.readByteArray32()),
    priority: sia.readUInt8(),
    encrypted: sia.readBool(),
  };
}

// Corresponding serializer for reference
function serializeMessage(msg: Message): Uint8Array {
  const sia = new Sia();

  sia
    .addUInt8(msg.version)
    .addUInt64(msg.timestamp)
    .addString8(msg.sender)
    .addString8(msg.recipient)
    .addString32(msg.body)
    .addArray8(msg.attachments, (s, att) => s.addByteArray32(att))
    .addUInt8(msg.priority)
    .addBool(msg.encrypted);

  return sia.toUint8Array();
}

// Round-trip test
const original: Message = {
  version: 1,
  timestamp: Date.now(),
  sender: "alice",
  recipient: "bob",
  body: "Hello from Sia!",
  attachments: [new Uint8Array([0x01, 0x02, 0x03])],
  priority: 5,
  encrypted: false,
};

const bytes = serializeMessage(original);
const decoded = deserializeMessage(bytes);
// decoded deeply equals original

Common Patterns