@digta/codec
v0.1.0
Published
Zero-dependency message framing (length-prefix + delimiter) and typed binary struct read/write. Pairs with @digta/network.
Maintainers
Readme
@digta/codec
Zero-dependency message framing and typed binary struct read/write for Node. The missing half of raw TCP: turn a byte stream into whole messages, and read/write fixed-layout binary (Modbus registers, custom device protocols).
npm install @digta/codecFraming — byte stream → whole messages
TCP has no message boundaries: one send can arrive as three chunks, or three sends as one. A framer buffers partial data and hands you only complete frames.
Length-prefixed
import { lengthPrefix } from "@digta/codec";
// or: const { lengthPrefix } = require("@digta/codec");
const framer = lengthPrefix({ bytes: 2 }); // 2-byte big-endian length header
// pairs directly with @digta/network:
conn.on("message", framer.feed);
framer.on("frame", (msg) => handle(msg)); // msg = payload without the prefix
conn.send(framer.encode("hello")); // prepends the length headerOptions: bytes (1 | 2 | 4, default 2), endian ('be' | 'le', default 'be'), includePrefix (does the length count its own header? default false), maxFrame (reject oversized frames, default 10 MB).
Delimiter
import { delimiter } from "@digta/codec";
const lines = delimiter("\n"); // or "\r\n", or a Buffer
conn.on("message", lines.feed);
lines.on("frame", (line) => console.log(line.toString()));
conn.send(lines.encode("PING")); // appends the delimiterFunctional style (no events)
push() is the core — feed a chunk, get back complete frames, the remainder stays buffered:
for (const frame of framer.push(chunk)) handle(frame);struct — typed binary read/write
Describe a fixed layout once, then decode/encode Buffers against it. Field order = byte order.
import { struct } from "@digta/codec";
const packet = {
id: "uint16be",
temp: "int16be", // e.g. tenths of a degree, can be negative
running: "bool",
name: { type: "string", length: 8 },
regs: { type: "array", of: "uint16be", length: 4 },
};
const obj = struct.read(buffer, packet); // → { id, temp, running, name, regs }
const buf = struct.write(obj, packet); // → Buffer
struct.size(packet); // → byte lengthTypes
uint8 int8 · uint16 int16 uint32 int32 · float32/float float64/double · bool
Add be/le for endianness (uint16le, float32be, …). Bare names are big-endian — the network / Modbus default. Plus:
{ type: "string", length, encoding? }— fixed-width text (trailing NULs trimmed on read){ type: "bytes", length }— raw Buffer slice{ type: "array", of, length }— repeated scalar (e.g. a block of Modbus registers)
Why it pairs with @digta/network
@digta/network deliberately leaves TCP framing to you (it's protocol-specific). This is that layer:
import { createConnection } from "@digta/network";
import { lengthPrefix, struct } from "@digta/codec";
const conn = createConnection({ url: "tcp://192.168.1.10:9000" });
const framer = lengthPrefix({ bytes: 2 });
conn.on("message", framer.feed);
framer.on("frame", (buf) => {
const reading = struct.read(buf, { sensor: "uint16be", value: "float32be" });
logger.info(reading);
});
conn.connect();License
MIT
