Skip to content

Repository files navigation

@matteophre/binlayout

Zero-dependency TypeScript library for deterministic binary message parsing, validation, and layout composition.

binlayout helps you describe binary protocols in a declarative way and turn them into strongly typed, predictable parsers without pulling in runtime dependencies.

Why use it

  • deterministic parsing for binary protocols
  • strict TypeScript inference for schema outputs
  • near-zero-allocation hot path via parseInto() (exactly zero for numeric-only schemas; see below for bytes/ascii fields)
  • support for validation, dynamic layouts, checksums, and framing helpers
  • transport-independent core design that keeps framing concerns separate

Installation

npm install @matteophre/binlayout

Quick start

import { compileSchema, defineSchema } from '@matteophre/binlayout';

const schema = defineSchema({
  name: 'ModbusRTUMessage',
  endianness: 'LE',
  fields: [
    { name: 'slaveId', type: 'uint8' },
    { name: 'functionCode', type: 'uint8' },
    { name: 'startAddr', type: 'uint16' },
    { name: 'quantity', type: 'uint16' },
    { name: 'crc', type: 'uint16' },
  ],
});

const compiled = compileSchema(schema);
const buffer = new Uint8Array([0x01, 0x03, 0x00, 0x00, 0x00, 0x0a, 0x44, 0x39]);

const message = compiled.parse(buffer);
console.log(message);

The parsed object is inferred from the schema, so you get native TypeScript types without extra annotations.

Zero-alloc parsing with parseInto()

For high-frequency polling, reuse a target object across calls instead of allocating a new result every time:

const target: typeof message = {} as typeof message;

for (let i = 0; i < 1_000_000; i++) {
  compiled.parseInto(buffer, target);
  // use target.slaveId, target.crc, etc.
}

parseInto() reuses the target object: it performs zero additional allocations for schemas with only numeric fields. bytes fields also avoid allocating a new Uint8Array view when the target already holds one of the correct length from a previous call (the bytes are copied in place); ascii fields still allocate a string on every call, since JS strings are immutable. See src/benchmark.ts (npm run benchmark) for the methodology and measured numbers.

Validation example

import { createValidationStrategy } from '@matteophre/binlayout';
import { crc16xmodem } from 'crc';

const strategy = createValidationStrategy<number>({
  name: 'crc16-xmodem',
  compute(data) {
    return crc16xmodem(Buffer.from(data)) & 0xffff;
  },
});

const payload = buffer.subarray(0, buffer.length - 2);
const expected = (buffer[6]! << 8) | buffer[7]!;

console.log(strategy.verify(payload, expected));

Supported building blocks

  • numeric fields: uint8, uint16, uint32, int8, int16, int32, float32, float64
  • binary payloads: bytes
  • strings: ascii
  • variable-length bytes/ascii fields via lengthFrom, which can point to any earlier numeric field in the schema, not just the immediately preceding one
  • dynamic layouts and conditional layouts
  • checksum wrappers and validation strategies
  • framing helpers for transport-specific envelopes

Project status

The current focus is stability and polish for the 1.x line. The public contract is intended to remain predictable, and experimental helpers are clearly separated from the core layout semantics.

License

MIT

About

Zero-dependency TypeScript library for defining and parsing binary protocols with full type

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages