OmniBuf
for TypeScript & JS.
Runtime and code generator for Node.js, browsers, and edge workers: serialize declarative TypeScript schemas, drop in on @bufbuild/protobuf descriptors, or generate single-pass codecs and lazy Opaque views (see the TypeScript & JS benchmarks below).
$ pnpm add @omnibuf/core$ pnpm add @omnibuf/web$ pnpm add @omnibuf/nodeThree ways to use OmniBuf in TypeScript.
Define schemas directly in TypeScript, pass existing @bufbuild/protobuf descriptors with no build changes, or generate single-pass codecs and lazy Opaque views with the compiler plugin.
Declarative TypeScript schemas
Define a schema in TypeScript with defineMessage (or decorate a class with @omnibufMessage and @omnibufField) and pass it to create, toBinary, and fromBinary:
fieldNumsets the wire field number (1..65535).namesets the wire field name written to Field #0 (defaults tosnake_caseof the TypeScript property name).typeselects the wire encoding (uint32,uint64,sint32,sint64,fixed32,fixed64,float,double,bool,string,bytes,message, ormap).omitEmptyskips encoding when the field holds its default value.
import {
defineMessage,
create,
toBinary,
fromBinary,
type Message,
} from '@omnibuf/core';
interface Peer extends Message<'app.Peer'> {
peerId: number;
router: string;
active: boolean;
prefixes: number[];
}
const PeerSchema = defineMessage<Peer, 'app.Peer'>('app.Peer', {
peerId: { fieldNum: 1, name: 'peer_id', type: 'uint32' },
router: { fieldNum: 2, type: 'string', omitEmpty: true },
active: { fieldNum: 3, type: 'bool' },
prefixes: { fieldNum: 4, type: 'uint32', repeated: true },
});
const peer = create(PeerSchema, {
peerId: 64512,
router: 'rtr-01',
active: true,
});
const pkt = toBinary(PeerSchema, peer);
const out = fromBinary(PeerSchema, pkt);Existing @bufbuild/protobuf schemas
Already generating TypeScript descriptors with protoc-gen-es? Pass your existing DescMessage schemas directly to create, toBinary, fromBinary, and mergeFromBinary with no extra plugin step.
OmniBuf compiles each DescMessage into a cached field-dispatch table on first use, preserving unknown fields in $unknown and supporting both proto3 and Editions 2023/2024 descriptors.
import {
create,
toBinary,
fromBinary,
} from '@omnibuf/core';
// Standard protoc-gen-es descriptor:
import { PeerUpdateSchema } from './gen/peer_pb.js';
const msg = create(PeerUpdateSchema, {
asn: 64512,
routerId: 'edge-rtr-fra01',
active: true,
});
const pkt = toBinary(PeerUpdateSchema, msg);
const out = fromBinary(PeerUpdateSchema, pkt);Generated code with protoc-gen-omnibuf-es
Add protoc-gen-omnibuf-es to your buf.gen.yaml. The plugin generates typed Message interfaces, single-pass encoders and switch-based decoders, and lazy Opaque wrapper classes.
Opaque views read individual fields directly from the Uint8Array packet via locateField and re-serialize modified packets using spliceFields without decoding untouched fields.
version: v2
plugins:
- local: protoc-gen-omnibuf-es
out: src/gen
opt: target=tsimport {
create,
toBinary,
fromBinary,
} from '@omnibuf/core';
import {
PeerUpdateSchema,
OpaquePeerUpdate,
} from './gen/peer_omnibuf.pb.js';
const msg = create(PeerUpdateSchema, {
asn: 64512,
routerId: 'rtr-01',
active: true,
});
const pkt = toBinary(PeerUpdateSchema, msg);
// Lazy Opaque view: O(1) hop + zero-decode splice
const view = new OpaquePeerUpdate(pkt);
if (view.getActive() && view.getAsn() === 64512) {
view.setSequence(99n);
const forwarded = view.marshal();
}TypeScript & JS benchmarks.
Measured against protobufjs, @bufbuild/protobuf v2 (protobuf-es), google-protobuf, @msgpack/msgpack, and native JSON on the canonical 11-field PeerUpdate workload. Shorter bars = Faster.
Measures serializing the canonical 11-field PeerUpdate message (nested Location submessage, 4 Neighbor submessages, packed uint32 prefixes, string tags, and 64-bit counters) into a Uint8Array. OmniBuf writes in a single forward pass with 2-byte length backpatching and an inline UTF-8 fast path.
Measures decoding the full 11-field PeerUpdate packet back into a typed JavaScript message object. Generated OmniBuf schemas dispatch on Prefix-Varint tags in a single switch loop without reflection, reading strings and submessages in place without intermediate subarray slices.
Measures extracting a nested string field (peer.location.city) from a serialized packet. OmniBuf hops across preceding field headers in O(1) time per field using TZCNT length prefixes without decoding untouched strings, arrays, or submessages.
Measures updating a 64-bit sequence counter on an in-flight packet and producing the updated wire bytes. OmniBuf splices the modified field directly onto the raw Uint8Array packet without decoding or re-encoding untouched fields.
Benchmark Environment
pnpm run benchAll benchmarks serialize, deserialize, inspect, and mutate the same 11-field PeerUpdate message used in the Go benchmark suite: unsigned and ZigZag-signed integers, booleans, IEEE-754 floats, string lists, packed uint32 prefix arrays, a 6-field nested Location sub-message, and a 4-element repeated Neighbor sub-message list.
The benchmark harness is located in benchmarks/bench.ts in the omnibuf-es repository and can be reproduced locally with pnpm run bench or just bench.
TypeScript & JS questions.
How does omnibuf-es fit into a protobuf-es (@bufbuild/protobuf) codebase?
Messages in @omnibuf/core follow the @bufbuild/protobuf v2 plain-object design: every message has a $typeName property, optional $unknown field storage, and camelCase field properties that map to snake_case names in Field #0.
The standard lifecycle functions (create, toBinary, fromBinary, mergeFromBinary, clone, equals, and isMessage) accept existing protoc-gen-es descriptors, generated protoc-gen-omnibuf-es schemas, and declarative defineMessage schemas interchangeably.
When should I use Opaque views instead of fromBinary?
Use fromBinary when your code reads most fields of a message. Use generated Opaque views in edge workers, WebSocket gateways, and telemetry pipelines that only inspect a routing field or update a counter before forwarding the packet.
Getters hop across field headers in one step without decoding untouched strings or sub-messages, and view.marshal() splices modified fields directly onto the underlying Uint8Array.
const view = new OpaquePeerUpdate(pkt);
const router = view.getRouterId(); // O(1) header hop
// Update field #3 (active) and field #11 (sequence) in place:
view.setActive(false);
view.setSequence(42n);
const updated = view.marshal();How do I enable Field #0 inline field names or decode without a schema?
Pass { selfDescribing: true } to toBinary(schema, msg, options) on any schema to emit Field #0 (omnibuf.v1.SchemaMeta) at byte 0 with the root and nested field-name tables.
To inspect a packet without a schema or generated code, call decodeBlind(pkt). It returns a plain object keyed by field numbers, or by field names automatically when Field #0 is present.
import { toBinary, decodeBlind } from '@omnibuf/core';
// Prepend Field #0 (SchemaMeta) inline with the packet:
const pkt = toBinary(PeerSchema, peer, {
selfDescribing: true,
});
// Decode any OmniBuf packet without a schema:
const fields = decodeBlind(pkt);What is the difference between @omnibuf/core, @omnibuf/web, and @omnibuf/node?
@omnibuf/core is a zero-dependency, environment-agnostic runtime that works in browsers, Node.js, Deno, Bun, and Cloudflare Workers.
Importing @omnibuf/web registers a browser codec backed by TextEncoder.encodeInto and adds Web Streams (TransformStream) and DevTools packet inspection helpers. Importing @omnibuf/node registers a Buffer-backed UTF-8 codec, Node stream framing (writeFrame, readFrames), and .fds.json / .fds.pb.gz descriptor loaders.
// Browser / Edge: TextEncoder.encodeInto + Web Streams
import {
createOmniBufDecodeStream,
inspectPacket,
} from '@omnibuf/web';
// Node.js: Buffer UTF-8 codec + stream framing
import { writeFrame, readFrames } from '@omnibuf/node';