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

@aweftjs/codec

v0.1.1

Published

The normative encoding: values, deltas and commits to bytes, and back.

Readme

@aweftjs/codec

The wire format: values, deltas and commits to bytes, and back.

One value has one spelling. The decoder refuses any input an encoder would not have written, so two implementations that both pass the fixture suite write byte-identical output for the same commit. Byte equality is what conformance means here, and every rule in the package serves it.

That is a rule about spelling, not about trust. See "What this does not do" before you rely on it for anything else.

The format is written down in spec/format.md, and this package is its normative implementation. Nothing here knows about observables, transports or storage.

Quickstart

import { createId, decodeCommit, encodeCommit, type Commit } from '@aweftjs/codec';

const doc = createId();

const commit: Commit = {
	deltas: [
		{ type: 'add', id: doc, ref: { kind: 'object', key: 'title' }, value: 'plan' },
		{ type: 'add', id: doc, ref: { kind: 'object', key: 'done' }, value: false },
	],
};

const bytes = encodeCommit(commit);
const back = decodeCommit(bytes);
// encodeCommit(back) is byte for byte the same as bytes. Always, for any input decodeCommit
// accepted.

encodeCommit sorts the deltas, so the same commit handed over in any order gives the same bytes. decodeCommit refuses deltas that arrive out of that order rather than sorting them: accepting both spellings would mean two byte strings decode to one commit, and re-encoding could no longer reproduce its input.

A delta names a slot, not a path

The target is id plus ref. There is no path anywhere in the format, so a delta means the same thing whatever else moved in the same commit.

{ kind: 'object', key: 'title' }        // a string slot
{ kind: 'array',  key: position }       // a position key, ordered as bytes
{ kind: 'map',    key: someId }         // keyed by identity

ref.kind says which observable kind the delta is talking about, so a receiver that has never seen the target can still read the delta. value is present for add and replace and absent for remove.

Values are flat

type Value = null | boolean | number | string | Uint8Array | Reference;

There is no nested structure in a slot. A structure inlined into a slot would be state that no delta addresses, and every change to state is a delta. A slot that holds another observable holds a Reference to it:

{ edge: 'attach', kind: 'object', id: childId }   // where the child lives
{ edge: 'alias',  kind: 'object', id: childId }   // a second name for it, moves nothing

An observable has exactly one attach edge. Every other reference to it is an alias, which is what makes one walk up the attach edges the whole answer to where something sits.

Ids and positions

Ids are 12 random bytes, and idToText gives the 16 base64url characters that are safe in a URL, a log line or a JSON key. createId takes no seed and no injectable generator on purpose: code that needs deterministic ids takes them as input.

A position is a non-empty byte string that does not end in a zero byte. Those two rules are what guarantee a key always exists between any two distinct keys, so an array never runs out of room to grow. comparePositions is the plain unsigned byte comparison, and a prefix sorts before what extends it.

import { assertPosition, comparePositions, isValidPosition } from '@aweftjs/codec';

isValidPosition(Uint8Array.of(0x80));        // true
isValidPosition(Uint8Array.of(0x80, 0x00));  // false, ends in a zero byte

assertId and assertPosition hand the value back, so they can wrap it on the way into a structure rather than sitting on a line of their own.

Id, Position and Tag are three distinct types, not three spellings of Uint8Array. They are the same bytes at runtime and on the wire; the difference exists only while the compiler is looking, so a signature that wants an id refuses a position (design 104). Bytes from anywhere else become an id or a position by going through assertId or assertPosition, which is the check they needed anyway.

const id: Id = assertId(bytes);          // checked, and now typed as an id
const key: Position = assertPosition(k); // same, for an array slot key

This package judges positions and never mints one. Choosing a key between two others is left open by the format on purpose, and @aweftjs/core is what makes the choice: ordinary array work does it for you, so list.splice(1, 0, x) on ['a', 'c'] turns positions 80 81 into 80 8080 81. positionsOf reads the keys back, and insertAt is the other direction, for a receiver honouring a position it was told rather than inventing one and diverging. Writing your own is a real job, not a one-liner, so reach for core rather than starting from comparePositions.

Refusals name a reason

Every throw is a CodecError carrying a reason, and the reason is part of the format's contract rather than a message for a human. Two implementations that reject the same input for different stated reasons have not agreed on the format.

import { decodeCommit, type CodecError } from '@aweftjs/codec';

try {
	decodeCommit(damaged);
} catch (error) {
	(error as CodecError).reason;  // 'deltas-out-of-order', 'truncated', 'duplicate-slot', ...
}

The reasons are pinned by spec/fixtures/invalid/: one file per case, naming the bytes, the stage the refusal is due at, and the reason to refuse for. The filenames say which is which, so that directory is the list to read and to branch on. A reason not exercised there is not part of the contract.

The value layer

encodeValue and decodeValue sit below commits, on the small type set the format allows: null, booleans, integers, float64, byte strings, text strings and arrays. No maps and no tags, so there is no dialect to negotiate.

Integers are exact and written as integers in [MIN_INT, MAX_INT], which is +/- 2^53, and anything outside that is a float. The bound is exact representability rather than JavaScript's safe-integer range, because a double holds 2^53 exactly. A whole number written as a float is refused, and so is an integer padded into a wider form than it needs.

Most callers want encodeCommit and never touch this layer. It is public because an implementation in another language checks its own value encoding against this one.

What this does not do

It does not detect tampering. Damage a byte and one of two things happens: the bytes stop being a commit and decoding throws, or they are still a legal commit and decode cleanly into a different one. Both are correct. Flip a bit inside an id and you get a different id, which is as valid as the one you started with, and nothing in the format can know you did not mean it. Flipping every bit of three small commits in turn, 562 of 968 flips decoded without complaint. How many depends on the shape of the commit, and it is never small. If your bytes cross a channel that can change them, put a checksum or a signature in your own framing. This package will not notice.

Commit.tag is not that check, whatever the word integrity suggests. It is a digest over the prior values of the slots a commit addresses, computed by the sender against its own state before the commit, and it answers one question: did this commit land on the state the sender thought it would? A receiver that computes a different tag treats the two replicas as diverged and resynchronizes. It says nothing about the bytes in between, and the algorithm that fills it is still open, so nothing here computes or checks one yet.

It does not frame anything. A decoded commit is internally complete, but no public API reports how many bytes it consumed, so a log of commits needs its own framing to say where each one ends. The simplest workable one is a length prefix per frame:

const frame = (bytes: Uint8Array): Uint8Array => {
	const out = new Uint8Array(4 + bytes.length);
	new DataView(out.buffer).setUint32(0, bytes.length);
	out.set(bytes, 4);
	return out;
};
// Reading back: take the length, slice the frame, hand exactly that slice to decodeCommit.

decodeCommit takes one commit with nothing before or after it, so the framing decides where a commit ends, and anything that must survive a hostile channel adds its own checksum or signature at this layer. recipes/codec/main.ts is a complete log doing this.

Boundaries

Deliberately not here: any document model, any transport, any persistence.

The reasoning behind the choices lives in docs/design/, and the format they implement in spec/format.md.

The design notes

A design NNN above is the note of that number in docs/design/, which says what was decided, why, what it costs, and what would reverse it.