npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zzzeros0/nbsp

v0.5.3

Published

Zero-copy fixed-size binary structures for NodeJS TypeScript

Readme

NBSP

NodeJS Binary Structure Payloads

This package facilitates the creation and management of fixed binary payloads in NodeJS.

Why NBSP?

NBSP is designed for:

  • Binary network protocols (MQTT, CAN, custom TCP/UDP protocols)
  • Fixed-size payloads
  • Embedded / low-level communication
  • Interoperability with C/C++ structures

It avoids:

  • JSON serialization overhead
  • Dynamic allocations
  • Hidden copies

Example

interface AckPacket {
  id: string;
} // domain AckPacket

interface Packet {
  header: byte;
  id: string;
  method: string;
  content: string;
} // domain Packet

// Transformers
// Transforms string <-> number[]
const stringTransform = {
  input: [(data: string) => toBytes(data)],
  output: [(data: bytes) => toString(data)],
};

// Transforms hex string <-> hex number[]
const hexStringTransform = {
  input: [(data: string) => toBytes(data, true)],
  output: [(data: bytes) => toString(data, true)],
};

const AckPacketStruct = struct<AckPacket, Transformers<AckPacket>>(
  {
    id: charDataType(6),
  },
  {
    packed: true,
    transform: {
      id: hexStringTransform,
    },
  },
); // AckPacketStruct Constructor (class)

const PacketStruct = struct<Packet, Transformers<Packet>>(
  {
    header: DataType.UINT32LE,
    id: charDataType(6),
    method: charDataType(4),
    content: charDataType(64),
  },
  {
    packed: true,
    transform: {
      id: hexStringTransform,
      method: stringTransform,
      content: stringTransform,
    },
  },
); // PacketStruct Constructor (class)

console.log(AckPacketStruct.size); // 6 (Unpacked: 10)
console.log(PacketStruct.size); // 78 (Unpacked: 128)

mqttClient.on("message", (topic, msg) => {
  const packet = PacketStruct.from(msg);

  processMessage(topic, packet.id, packet.method, packet.content).then(() => {
    const ackMessage = AckPacketStruct.from(msg, sizeof(DataType.UINT32LE)); // Offset the 'header' property

    mqttClient.publish("ack", ackMessage.data());
  });
});

Struct Constructor methods

| Method | Description | Arguments | Returned type | | ------- | ----------------------------------------------------------------- | -------------------------------------------- | ------------- | | from | Creates a new instance and copies the buffer content from target. | (target: Buffer \| Struct, byte: byte = 0) | Struct<T> | | partial | Same as new; creates an instance with partial arguments. | (args: Partial<T>) | Struct<T> | | toJson | Returns a plaing object. | (target: Buffer, offset: byte = 0) | T |

Struct methods

| Method | Description | Arguments | Returned type | | ------ | ---------------------------------------- | -------------------------------------------------------------- | ------------- | | data | Returns the internal buffer (no copy). | | Buffer | | reset | Zero the internal buffer content. | | void | | toJson | Returns a plain object. | | T | | copy | Copies the buffer's content from target. | (target: Buffer \| Struct, offset: byte = 0, size: byte = 0) | void |

Struct Options

Options argument for the struct function.

| Property | Description | Type | Default | | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------- | | packed | When false, fields of the struct are aligned or padded. This is really important especially for binary communications, improving performance and usage. | boolean | false | | transform | An object that contains keys from the domain object. Transforms the data obtained/retrieved from the buffer data. | Transformers<T> | {} |

Usage

  1. Create a domain interface. This will represent your data.
interface Person {
  age: byte;
  name: string;
} // domain Person
  1. Create a Struct for your domain interface. Calling struct will return a new class; it is not intended to be extended.
const PersonStruct = struct<Person>({
  age: DataType.UINT8,
  name: charDataType(4), // strings are arrays of UINT8. charDataType(n) is equivalent to [DataType.UINT8, n]
});
  1. Now, you can instantiate the struct by using new or the StructConstructor's static method from. Instance exposes getters and setters to update and retrieve data directly from the fields of the struct in the buffer as well as some other methods to manage it:
const person1 = new PersonStruct({
  age: 24,
  name: toBytes("Dave"), // Transform the string to UTF-8 UINT8 array
});
console.log("Name: %s, age: %d", toString(person1.name), person1.age); // Name: Dave, age: 24

const person2 = PersonStruct.from(Buffer.from("63000000526f7365", "hex"));

console.log("Name: %s, age: %d", toString(person2.name), person2.age); // Name: Rose, age: 99

console.log(person2.data()); //  <Buffer 63 00 00 00 52 6f 73 65>

console.log(person2.toJson()); // { age: 99, name: [ 82, 111, 115, 101 ] }

const person3 = PersonStruct.toJson(Buffer.from("5a0000004a616b65", "hex"));

console.log("Name: %s, age: %d", toString(person3.name), person3.age); // Name: Jake, age: 90

Transformers help to transform data. The second generic argument provided to struct defines the properties to be transformed. When a property has transformers, its exposed TypeScript type becomes the transformed type instead of the raw BindedType. You can have multiple transforms in input/output; each one will receive the last transformed value.

const PersonStruct = struct<Person, { name: PropertyTransform }>(
  {
    age: DataType.UINT8,
    name: charDataType(4),
  },
  {
    transform: {
      name: {
        // Executed when data is written to the buffer
        input: [(data: string) => toBytes(data)],
        // Executed when data is retrieved from the buffer
        output: [(data: bytes) => toString(data)],
      },
    },
  },
);
const person = new PersonStruct({
  age: 24,
  name: "Dave", // Input transformer
});
person.name = "Jack"; // Input transformer
console.log(person.name); // "Jack", output transformer

Arrays & Nesting

Array types are defined with a type and a fixed length of items:

const PersonStruct = struct<Person>({
  name: [DataType.UINT8, 4], // or charDataType(4)
});

Strings are represented as a fixed array of UINT8 if no transformer was applied for that property. Use the shorthand charDataType.

You can nest Structs:

interface DeviceConfig {
  mode: byte;
  factor: byte;
} // domain DeviceConfig

interface Device {
  id: string;
  config: DeviceConfig; // Provide the domain type
} // domain Device

const DeviceConfigStruct = struct<DeviceConfig>({
  mode: DataType.UINT8,
  factor: DataType.UINT8,
});

const DeviceStruct = struct<Device>({
  id: charDataType(6),
  config: DeviceConfigStruct, // Provide the struct as type
});

const instance = new DeviceStruct({
  id: Array.from(randomBytes(6)),
  config: {
    // Create a plain object
    // DO NOT use DeviceConfigStruct constructor
    // it will cause unnecessary allocations
    mode: 4,
    factor: 8,
  },
});

console.log(instance.config.factor); // 8

You can also store Structs or Arrays inside of Arrays:

interface Group {
  people: Person[];
} // domain Group

const GroupStruct = struct<Group>({
  people: [PersonStruct, 100],
});

const groupInstance = new GroupStruct({
  people: [
    // Create a plain object
    // DO NOT use PersonStruct constructor
    // it will cause unnecessary allocations
    {
      name: "Jack",
    },
    {
      name: "Rose",
    },
    {
      name: "Dave",
    },
    ...
  ],
});

console.log(groupInstance.people); // Array<100> [ { name: [Getter/Setter] }, ... ]

Important

Structs must be instantiated with plain objects when are nested, do not use the Struct's constructor; doing so will make unnecessary allocations.

JSON

Instances can be converted to plain JavaScript object with all the resolved properties and nested attributes of your struct using toJson method:

personInstance.toJson(); // { name: [ 68, 97, 118, 101 ], age: 24 }

You can serialize directly to a plain JavaScript object from a buffer by using the static method toJson:

PersonStruct.toJson(Buffer.from("4a616b655a000000", "hex")); // { name: [ 68, 97, 118, 101 ], age: 24 }

Important

Transformers will also run when serializing JSON.

Prefer the static toJson method when working with raw buffers; there's no point on doing this:

PersonStruct.toJson(personInstance.data());
// Instead
personInstance.toJson();

Endianness

Data types defined in the enum DataType difference between LE and BE types:

structure({
  property1: DataType.UINT32BE,
  property2: DataType.INT64LE,
});

Note

You can get the size of data types with sizeof:

console.log(sizeof(DataType.INT16LE)); // 2
console.log(sizeof(charDataType(6))); // 6
console.log(sizeof(Structure)); // Or Structure.size

Floating point (FLOAT32) precision

NBSP uses IEEE-754 floating point representations for FLOAT32 and FLOAT64, exactly like C, C++, Rust, Java, etc. This means that some decimal values cannot be represented exactly in binary.

Why does 0.4 become 0.4000000059604645?

FLOAT32 is a 32-bit single-precision IEEE-754 float.

Some decimal numbers, like 0.4, cannot be represented exactly using a finite binary fraction, so the closest representable value is stored instead.

Example:

const value = 0.4;

instance.floatValue = value;
console.log(instance.floatValue); // 0.4000000059604645

This is not a bug in NBSP. It is the actual value stored in memory and this behavior is universal

The same thing happens in other languages:

float x = 0.4f;
printf("%.17f\n", x); // 0.4000000059604645

NBSP intentionally exposes the real binary value, without rounding or post-processing, to ensure:

  • Full transparency
  • Bit-exact compatibility with C/C++ structures
  • Deterministic binary payloads

If you need to compare floating point values, never use strict equality:

a === b; // ❌ unsafe for floats

Instead, compare with a tolerance:

Math.abs(a - b) < 1e-6; // ✅ safe

Or, when possible:

  • Use integers (scaled values, fixed-point)
  • Use FLOAT64 if higher precision is required.