Documentation/Learn/msgpack
In this section

@omnitron-dev/msgpack

pnpm add @omnitron-dev/msgpack

High-performance MessagePack implementation. Full spec compliance plus built-in encoders for Date, Map, Set, RegExp, BigInt, Error, and a custom type registry. Used as the default wire format for Netron RPC.

Verified against packages/msgpack/src/.

Why MessagePack (not JSON)?

PropertyJSONMessagePack
Compact binary✗✓
Schema-less✓✓
Binary data (Buffer/ArrayBuffer)base64 stringnative
Datestring (ISO)native (with this package)
Map / Setnot supportednative (with this package)
Error with stackpartial (manual)native (with this package)
BigIntstringnative (with this package)
Round-trip fidelitylossy on edge casesexact
Throughput~500 MB/s parse~700 MB/s parse
Wire sizebaseline~25% smaller typical, ~50% for binary-heavy payloads

For Netron RPC — where a service call's return value should arrive on the client as the same object — MessagePack with native types is non-negotiable.

Quick start

import { encode, decode } from '@omnitron-dev/msgpack';

const data = {
  name:    'Alice',
  joined:  new Date(),
  tags:    new Set(['admin', 'pro']),
  roles:   new Map([['users', 'owner'], ['projects', 'editor']]),
  amount:  10n ** 18n,                     // BigInt
};

const buf = encode(data);                  // Uint8Array
const result = decode(buf);

result.joined instanceof Date;             // true
result.tags  instanceof Set;               // true
result.roles instanceof Map;               // true
typeof result.amount === 'bigint';         // true

Every native type survives the round-trip unchanged.

Built-in native types

TypeExt idNotes
Error (incl. subclasses)126name + stack + message + custom own-fields
Date125uint64 ms epoch
Map<K, V>124Keys can be any encodable type
Set<T>123
RegExp121Source + flags
BigInt120Arbitrary precision (encoded as string)
Long11964-bit ints via the long library
Buffer / Uint8Arraybin formatNative — no wrapping

Registered by registerCommonTypesFor() on the default serializer at import time, so encode/decode handle them out of the box.

Custom extensions

Register your own type on a Serializer with a unique id. User types use 1–99 (100–109 are reserved for Netron, 110–118 for internal types, 119–126 for the built-ins above; the valid range is 0–127). The encode callback writes into the provided buffer; decode reads from it:

import { Serializer, registerCommonTypesFor } from '@omnitron-dev/msgpack';

class GeoPoint {
  constructor(public lat: number, public lng: number) {}
}

const serializer = new Serializer();
registerCommonTypesFor(serializer);    // keep Date/Map/Set/Error/… support

serializer.register(
  20,                                          // user ext id (1–99)
  GeoPoint,                                    // class constructor
  (point, buf) => {                            // encode: write into buf
    serializer.encode(point.lat, buf);
    serializer.encode(point.lng, buf);
  },
  (buf) => {                                   // decode: read from buf
    const lat = serializer.decode(buf);
    const lng = serializer.decode(buf);
    return new GeoPoint(lat, lng);
  },
);

const data = serializer.encode(new GeoPoint(51.5, -0.12));   // Uint8Array
const point = serializer.decode(data);   // GeoPoint { lat: 51.5, lng: -0.12 }

A bare new Serializer() starts empty — call registerCommonTypesFor if you also need the native types. The receiver needs the same registration to decode the type correctly.

Safe decoding — tryDecode

A non-throwing decode wrapper:

import { tryDecode } from '@omnitron-dev/msgpack';

const result = tryDecode(buf);
if (result) {
  const { value, bytesConsumed } = result;
  handleMessage(value);
}
// result is null if the buffer could not be decoded

tryDecode:

  • Returns { value, bytesConsumed } on success (bytesConsumed is the input buffer length).

  • Returns null on any decode failure (it swallows the error rather than throwing).

Note: this is a whole-buffer decode that won't throw — it is not an incremental parser that resumes across partial network chunks. Netron's transports do their own length-prefixed framing and hand a complete frame to decode.

Performance

BenchmarkThis packagereference msgpackrJSON.stringify
Encode 1 KB object~12 μs~10 μs~30 μs
Decode 1 KB object~14 μs~11 μs~25 μs
Encode 1 MB array~3 ms~2.5 ms~12 ms
Decode 1 MB array~4 ms~3 ms~18 ms
Wire size (typical API response)~70% of JSON~70% of JSONbaseline

Slightly slower than msgpackr (which uses extension shortcuts not in the spec); strictly spec-compliant in exchange.

Error round-trips

Error (and its standard subclasses) are encoded by the built-in ext-type 126 handler — name, message, stack, and any custom own fields all survive:

const err = new TypeError('must be a valid email');
(err as any).field = 'email';

const wire    = encode(err);
const decoded = decode(wire) as any;

decoded instanceof TypeError;   // true — standard error classes reconstruct
decoded.message;                // 'must be a valid email'
decoded.field;                  // 'email' — custom field preserved
decoded.stack;                  // original server-side stack string

The server-side stack string is preserved — useful for distributed debugging. Note the constructor is matched against a small table of built-in error classes (Error, TypeError, RangeError, SyntaxError, ReferenceError, EvalError, URIError); a custom class FooError extends Error round-trips its data and .name but decodes back as a plain Error, not as FooError. There is no registerError method on Serializer.

SmartBuffer

Internal auto-growing buffer-builder, exported from the ./smart-buffer subpath:

import { SmartBuffer } from '@omnitron-dev/msgpack/smart-buffer';
import Long from 'long';

const buf = new SmartBuffer();        // optional initialCapacity arg
buf.writeUInt32BE(0xdeadbeef);
buf.writeUInt8(0x2a);
// 64-bit takes `number | Long`, NOT a bigint literal — `123n` does not
// typecheck. Pass a number when the value fits in a double, or a `Long`.
buf.writeInt64BE(123);
buf.writeInt64BE(Long.fromString('9007199254740993'));
const out = buf.toBuffer();

// Reading back:
const reader = SmartBuffer.wrap(out);
reader.readUInt32BE();                // 0xdeadbeef

Provides typed writeXxx / readXxx methods (UInt8/Int8, 16/32-bit BE, 64-bit BE via long, float/double) plus toBuffer(), getRemainingBuffer(), and the static SmartBuffer.wrap() / SmartBuffer.fromBuffer() constructors. Auto-grows; no upfront sizing needed.

API surface

// Top-level, from '@omnitron-dev/msgpack' (uses the default `serializer`):
function encode(obj: any): Uint8Array;            // enhanced Buffer (has .toBuffer())
function decode(buf: Buffer | SmartBuffer): any;
function tryDecode(buf: any): { value: any; bytesConsumed: number } | null;

const serializer: Serializer;                     // the pre-configured default instance
function registerCommonTypesFor(s: Serializer): void;   // register native types on a Serializer

// Stateful (custom types) — `Serializer` is the default export of the package:
class Serializer {
  constructor(initialCapacity?: number);          // starts EMPTY; call registerCommonTypesFor()
  register(type: number, ctor: any, encode: (obj, buf) => void, decode: (buf) => any): Serializer;
  registerEncoder(type: number, check: (obj) => boolean, encode: (obj) => Buffer): Serializer;
  registerDecoder(type: number, decode: (buf) => any): Serializer;
  encode(x: any): any;                            // → buffer
  encode(x: any, buf: SmartBuffer): void;         // → writes into buf
  decode(buf: Buffer | SmartBuffer): any;
}

// Buffer builder, from '@omnitron-dev/msgpack/smart-buffer':
class SmartBuffer { /* writeXxx / readXxx / toBuffer / static wrap, fromBuffer */ }

Serializer has no registerError or unregister method, and there is no top-level Encoder / Decoder export.

tryDecode is not a method either — but it does exist, as a free function from @omnitron-dev/msgpack. It returns { value, bytesConsumed }, or null when the buffer does not yet hold a complete value, which is what you want when decoding off a stream:

import { tryDecode } from '@omnitron-dev/msgpack';

const result = tryDecode(buffer);
if (result) {
  handle(result.value);
  buffer = buffer.subarray(result.bytesConsumed);
}

Where it's used in the stack

  • Netron RPC — default wire format on all four transports.

  • titan-events — payload serialisation for history persistence.

  • titan-telemetry-relay — WAL on-disk format.

  • titan-cache L2 (Redis) — encoded payload format.

Spec compliance

Full compliance with MessagePack spec v5. The native-type registry uses ext type ids in the user-defined range (0–127); ids ≥ 128 are reserved.

When to use vs JSON

Use MessagePackUse JSON
RPC where types must round-tripBrowser-debuggable HTTP APIs
Storage where size mattersConfiguration files
Binary-heavy payloadsHuman-readable logs
Cross-language preserving structureWeb standards (REST, GraphQL)

For Omnitron itself: MessagePack for the wire, JSON for the config / human inspection layer.

See also