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

@brlebtag/binser

v0.1.0

Published

Compilador de schemas binários: gera serializadores bit a bit rápidos para JavaScript e TypeScript.

Readme

binser

Compilador de schemas binários para JavaScript e TypeScript. Você descreve o formato num arquivo de texto, o binser gera um módulo sem dependências com serialize, deserialize e size, e os dados são gravados bit a bit, sem nenhum metadado — o menor tamanho possível, com código especializado e rápido.

x: i8
y: i8
alive: bool
life: ui7
speed: f32
name: utf8
binser -s Player.txt -o Player.js
import { serialize, deserialize, size } from './Player.js'

const player = { x: 10, y: 20, alive: true, life: 100, speed: 30, name: 'Bruno' }
const dv = new DataView(new ArrayBuffer(Math.ceil(size(player) / 8)))   // 16 bytes

serialize({ dataview: dv, begin_in_bits: 0, obj: player })
const { obj } = deserialize({ dataview: dv, begin_in_bits: 0, obj: null })

O mesmo objeto em JSON ocupa 65 bytes.


Instalação

npm install -g @brlebtag/binser     # comando global "binser"

O compilador (comando binser) roda em Node.js 22+ ou Bun. O código gerado não depende de nada: usa só DataView, BigInt e TextDecoder, então roda em qualquer ambiente JavaScript moderno (browsers inclusive). A suíte de testes inteira roda em Node.js e em Bun (npm test e npm run test:bun).

Com Bun:

bun add -g @brlebtag/binser     # instala o comando "binser"
bunx --bun binser -s Player.txt -o Player.ts    # executa a CLI no próprio Bun

O arquivo .ts gerado pode ser importado direto no Bun, sem etapa de build.

Em projetos com mais de uma ponta (cliente e servidor, por exemplo), instale também como dependência de desenvolvimento com versão fixa, para que todos gerem código com a mesma versão:

npm install -D @brlebtag/binser
npx binser -s schemas/Player.txt -o src/generated/Player.ts

Linguagem de schema

Cada linha declara um campo (nome: tipo). Os campos são gravados na ordem em que aparecem. Comentários começam com #.

Tipos

| Tipo | Bits | Valor em JS / TS | | --------------------------------- | -------------------------------------- | ------------------------------------ | | iN, uiN (1 ≤ N ≤ 64) | N | number (N ≤ 53) ou bigint | | i8, ui16, i32, ui64, ... | casos particulares de iN/uiN | | | bool | 1 | boolean | | f16, f32, f64 | 16, 32, 64 (IEEE-754) | number | | q(min, max, passo) | ⌈log₂((max−min)/passo + 1)⌉ | number (quantizado) | | enum(a, b, c) | ⌈log₂K⌉ | 'a' \| 'b' \| 'c' | | utf8 (str, str8) | 32 + 8 × bytes | string | | utf16 (str16) | 32 + 16 × unidades | string | | utf8!, utf16! | tamanho em Exp-Golomb (poucos bits) | string | | utf8<=N, utf16<=N | tamanho em ⌈log₂(N+1)⌉ bits | string (no máximo N unidades) | | [T] | 32 + elementos | T[] | | [T]! | Exp-Golomb + elementos | T[] | | [T; N] | exatamente N elementos, sem contador | T[] | | [T; <=N] | ⌈log₂(N+1)⌉ + elementos | T[] | | (A, B, C) | A + B + C | [A, B, C] | | { a: A, b: B } | A + B | objeto | | ?T | 1 bit de presença (+ T) | T \| null | | union(A, B) | ⌈log₂K⌉ bits de tag + valor | { type: 'A', value: A } \| ... |

Inteiros com sinal usam complemento de dois (i2 vai de −2 a 1). Os detalhes de cada tipo estão em docs/FORMAT.md.

Números quantizados: q(min, max, passo)

Muitos números não precisam de toda a precisão de um f32. Uma posição num mapa de 0 a 4096 com precisão de 0,1 tem 40.960 passos: cabe em 16 bits em vez de 32.

x: q(0, 4096, 0.1)      # 16 bits; 1234.56 é gravado como 12346 e lido como 1234.6
angle: q(0, 360, 1.5)   # 8 bits
ratio: q(0, 1, 0.01)    # 7 bits

O erro máximo é de meio passo. Valores fora da faixa são saturados em min/max.

Enums

type State = enum(idle, moving, stopped)   # 2 bits
state: State
mode: enum(on, off)                        # 1 bit

Em JavaScript o valor é a string ('moving'); em TypeScript, uma união de literais. Enums nomeados também exportam a lista de valores (State → ['idle', 'moving', 'stopped']).

Opcionais e uniões

owner: ?ui32                 # null → 1 bit; 42 → 1 + 32 bits
event: union(Login, Chat)    # 1 bit de tag + o valor escolhido
serialize({ dataview, begin_in_bits: 0, obj: { owner: null, event: { type: 'Chat', value: { text: 'oi' } } } })

Tipos nomeados e imports

# Entity.txt
import Vec2 from "./Vec2.txt"              # a raiz de outro schema
import { State } from "./Common.txt"       # types declarados em outro schema

type Stats = { hp: ui7, mana: ui7 }

pos: Vec2
state: State
stats: Stats
path: [Vec2]!

Um arquivo pode ser uma união de tipos em vez de uma lista de campos — útil para mensagens:

# Message.txt
import Login from "./Login.txt"
import Chat from "./Chat.txt"
type Ping = {}
union Login, Chat, Ping

Endianness

O padrão é big-endian. Uma linha little faz os campos seguintes serem gravados em little-endian; big volta ao padrão. Só afeta larguras múltiplas de 8 com 16 bits ou mais (ui16, f32, i64...).

id: ui16          # 0x1234 → 12 34
little
checksum: ui32    # 0x11223344 → 44 33 22 11
big

API gerada

export const SCHEMA_HASH: string                  // muda sempre que o formato muda
export const FIXED_SIZE: number | null            // bits, se o schema tiver tamanho fixo
export function size(obj?: Player): number        // bits exatos de obj (obj é opcional em schemas fixos)
export function serialize(ctx: Context<Player>): Context<Player>
export function deserialize(ctx: Context<Player>): Context<Player>
export function inspect(ctx: Context<Player>): InspectResult<Player>   // só com --debug

interface Context<T> {
  dataview: DataView
  begin_in_bits: number   // bit onde começa; ao final, aponta para o bit seguinte ao valor
  obj: T | null
}
  • serialize e deserialize alteram e retornam o mesmo ctx, sem alocar memória extra.
  • deserialize com ctx.obj diferente de null preenche o objeto existente (inclusive objetos e arrays aninhados) — útil em loops de alta frequência para reduzir o trabalho do garbage collector.
  • Os bits do buffer fora do valor não são alterados, então vários valores (de schemas diferentes) podem ser gravados em sequência:
const ctx = { dataview, begin_in_bits: 0, obj: header }
Header.serialize(ctx)
ctx.obj = player
Player.serialize(ctx)    // começa no bit seguinte ao Header, sem padding

Com saída .ts, o módulo exporta também as interfaces (Player, Context, tipos nomeados).


CLI

binser -s <schema.txt> -o <saída.js|saída.ts> [opções]
binser explain <schema.txt>
binser dump -s <schema.txt> (<pacote.bin> | --hex "0A 14 ...") [--offset <bits>] [--json]

| Opção | Descrição | | ------------------ | --------------------------------------------------------------------------- | | -s, --schema | schema de entrada | | -o, --out | arquivo gerado; a extensão escolhe a linguagem (.js, .mjs, .ts, .mts)| | --dts | com saída JavaScript, gera também o .d.ts | | --checked | valida os valores no serialize (use em desenvolvimento e testes) | | --debug | exporta também inspect(ctx) | | -w, --watch | gera de novo sempre que o schema ou um import mudar |

Sem --checked, valores fora da faixa são cortados para o número de bits do campo. Com --checked, o serialize lança um erro com o caminho do campo:

RangeError: Player.path[3].x: 5000 não cabe em ui12 (0..4095)

Erros de schema indicam arquivo, linha e coluna:

erro: Player.txt:4:8: tipo 'ui0' inválido (N deve estar entre 1 e 64)

Entendendo os pacotes

O pacote não carrega metadados; toda a informação vem do schema e destas ferramentas.

binser explain mostra o layout:

Player   FIXED_SIZE = null (tamanho variável; parte fixa inicial: 56 bits)

offset  bits     campo  tipo
     0  8        x      i8
     8  8        y      i8
    16  1        alive  bool
    17  7        life   ui7
    24  32       speed  f32
    56  32 + 8n  name   utf8

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       x       |       y       |a|    life     |     speed     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                 speed (cont.)                 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
seguido de: name (utf8)

binser dump decodifica um pacote campo a campo:

$ binser dump -s Player.txt --hex "0A 14 E4 41 F0 00 00 00 00 00 05 42 72 75 6E 6F"
bit  len  campo     tipo  bits                                 valor
  0    8  x         i8    00001010                             10
  8    8  y         i8    00010100                             20
 16    1  alive     bool  1                                    true
 17    7  life      ui7   1100100                              100
 24   32  speed     f32   01000001 11110000 00000000 00000000  30
 56   32  name.len  ui32  00000000 00000000 00000000 00000101  5
 88   40  name      utf8  42 72 75 6E 6F                       "Bruno"
total: 128 bits (16 bytes)

Se o pacote estiver corrompido, o dump mostra os campos lidos até o erro. Com --debug, o módulo gerado exporta inspect(ctx), que devolve essa mesma lista em runtime. O topo de todo arquivo gerado traz a tabela de layout como comentário.

SCHEMA_HASH: as duas pontas de uma comunicação podem trocar o hash ao conectar e recusar a conexão se forem diferentes.


Segurança

deserialize foi feito para receber dados não confiáveis:

  • contadores de strings e arrays são comparados com os bits restantes antes de alocar memória — um contador de 4 bilhões gera erro imediato;
  • limites (<=N), índices de enum, tags de união e passos de q fora da faixa geram erro;
  • a leitura nunca passa do fim do DataView.

A memória alocada na leitura é proporcional ao tamanho do pacote, mas pode ser bem maior que ele: um [[ui8]!]! com milhares de arrays vazios gasta 1 bit por array no pacote e dezenas de bytes por array na memória. Em dados não confiáveis, prefira limites ([T; <=N], utf8<=N).

Chame deserialize dentro de try/catch; qualquer erro significa dado inválido.


Desempenho

npm run bench / bun bench/run.ts (Node 24 e Bun 1.4, Windows; operações por segundo, maior é melhor):

| Cenário | biblioteca | bytes | Node encode | Node decode | Bun encode | Bun decode | | ----------------------------------- | -------------- | ----- | ----------- | ----------- | ---------- | ---------- | | Player (6 campos, com string) | binser | 16 | 15,3 mi | 11,2 mi | 14,5 mi | 12,4 mi | | | protobufjs | 20 | 6,2 mi | 9,2 mi | 4,7 mi | 9,5 mi | | | msgpackr | 40 | 5,1 mi | 5,1 mi | 5,4 mi | 6,7 mi | | | JSON | 65 | 3,9 mi | 2,8 mi | 8,9 mi | 4,6 mi | | 64 entidades (ids, q, enum, bool) | binser | 486 | 1,41 mi | 778 mil | 1,36 mi | 931 mil | | | protobufjs | 1.596 | 86 mil | 223 mil | 203 mil | 299 mil | | | msgpackr | 4.298 | 69 mil | 47 mil | 81 mil | 34 mil | | | JSON | 5.432 | 55 mil | 50 mil | 153 mil | 102 mil | | 6 números alinhados em bytes | binser | 23 | 42,1 mi | 21,1 mi | 18,6 mi | 15,8 mi | | | DataView à mão | 23 | 41,3 mi | 33,1 mi | 21,9 mi | 22,3 mi |

Detalhes e histórico das otimizações em bench/README.md.

O código gerado é linear e especializado para o schema; campos alinhados em byte usam uma única chamada do DataView.


API programática

import { compile, compileSource, explain, dump } from '@brlebtag/binser'

const { code, dts, hash, fixedSize } = compile('schemas/Player.txt', { language: 'ts', checked: false, debug: false })
const result = compileSource('x: i8\ny: i8', { language: 'js' })

Desenvolvimento

npm install
npm test            # testes (inclui property-based e a saída TypeScript)
npm run test:bun    # a mesma suíte executada no Bun
npm run typecheck
npm run bench
npm link            # comando "binser" global apontando para este repositório

Licença

MIT © 2026 Bruno G. A. Lebtag