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

valuehash

v0.1.0

Published

Exact canonical value keys for JavaScript, with an optional compact 32-bit hash.

Downloads

152

Readme

valuehash

Exact canonical value keys for JavaScript. valueKey(a) === valueKey(b) means the supported values have the same structure; no digest collision is hiding behind that answer. valueHash is the explicitly lossy 32-bit layer. object-hash gives you a digest with collisions you cannot audit; valuehash gives you an exact canonical key, and a digest only if you ask.

import { valueKey } from 'valuehash'

const cache = new Map<string, unknown>()
const query = { filters: new Set(['open', 'mine']), page: 1 }
cache.set(valueKey(query), await runQuery(query))

const rebuilt = { page: 1, filters: new Set(['mine', 'open']) }
cache.get(valueKey(rebuilt)) // the cached result

Why this exists

Hash packages are useful digest functions, but a digest cannot provide an exact structural key. Some also quietly erase distinctions before hashing. These are live probes from npm run bench (Node 24.13.1):

| case | valueKey | object-hash | hash-it | node-object-hash | hash-sum | |---|---|---|---|---|---| | object key order | ✓ | ✓ | ✓ | ✓ | ✓ | | '1', 1, 1n, [1] | ✓ | ✓ | ✓ | ✓ | ✓ | | Set insertion order | ✓ | ✓ | ✓ | ✓ | ✓ | | different Set contents | ✓ | ✓ | ✓ | ✓ | ✗ collision | | different Map contents | ✓ | ✓ | ✓ | ✓ | ✗ collision | | ordered self-cycle | ✓ | ✓ | ✓ | 💥 throws | ✓ | | -0 / NaN (SameValue) | ✓ | ✗ | ✗ | ✗ | ✗ | | cycle through Set | △ throws by design | ✓ | ✓ | 💥 throws | ✓ |

The last row is not a victory for implementations that return a digest: canonicalizing a cyclic graph across unordered edges is graph canonization. valuehash refuses that input instead of making an insertion-order-dependent promise. DESIGN.md gives the constructive collision proof and the exact boundary.

The golden property

For supported values, modulo the two caveats below, the intended contract is both directions:

isoEqual(a, b)                  // implies valueKey(a) === valueKey(b)
valueKey(a) === valueKey(b)     // implies isoEqual(a, b)

Primitives use SameValue: NaN equals NaN, while -0 differs from 0. Objects ignore property insertion order, Sets and Maps ignore insertion order, and sparse holes differ from stored undefined.

The first deliberate caveat is sharing. isoequal decides sharing-sensitive graph isomorphism; valuehash decides the coarser unfolding structure. A reference reached again outside the current ancestor path is serialized again:

const x = {}
valueKey([x, x]) === valueKey([{}, {}]) // true
isoEqual([x, x], [{}, {}])              // false

That is necessary for a generally useful value key: incidental memoization or subtree reuse should not change it. Active cycles still use canonical path-relative back-references.

The second deliberate caveat is class erasure for declared-unordered iterables: anything speaking the protocol is keyed as the multiset of its iterated members, with its concrete class projected away — so a marked Bag([2, 1]) and a Set([1, 2])-shaped twin from a different class get the same key, while isoequal (which checks prototype identity first) distinguishes them. This is the protocol's point — the key names the contents, not the wrapper — but it means the reverse implication holds only up to that projection. One more sharp edge of the protocol: member iteration must be repeatable — a one-shot generator drains on the first key and produces a different key next time. And independently of the protocol, integer-typed TypedArrays are keyed by their underlying bytes, which follow platform endianness (little-endian on effectively all modern targets).

Supported values

The v0.1 domain is deliberately closed:

  • string, number, bigint, boolean, null, and undefined;
  • plain objects with Object.prototype or a null prototype, using enumerable string keys sorted lexicographically;
  • arrays by indexed slots, including exact sparse holes (custom properties are outside array identity);
  • Set and Map as unordered structural collections, including enumerable string properties on the collection object;
  • Date, RegExp, ArrayBuffer, TypedArrays, and boxed string/number/bigint/boolean;
  • any iterable declaring the unordered-collection protocol below; and
  • ordered cycles, plus cycles contained wholly inside one independently serialized unordered member.

Functions, symbols, WeakMap, WeakSet, Promise, DataView, detached buffers, unknown class instances, and unknown TypedArray subclasses throw. Symbol-keyed and non-enumerable properties are outside v0.1 structural identity. Built-in leaf objects use their logical value; ArrayBuffers use exact bytes and float TypedArrays normalize NaN payloads to SameValue semantics.

The unordered-collection protocol

valuehash honors the same package-neutral declaration as isoequal:

import { UNORDERED, valueKey } from 'valuehash'

class Bag<T> {
    readonly [UNORDERED] = true
    constructor(private items: T[]) {}
    *[Symbol.iterator]() { yield* this.items }
}

valueKey(new Bag([1, 1, 2])) === valueKey(new Bag([2, 1, 1])) // true

The logical identity of a declared collection is its iterated multiset; multiplicity counts and concrete class fields do not. Libraries that cannot carry the symbol can use createValueKey({ isUnorderedCollection }).

Worst case, honestly

Every unordered member is serialized independently and the resulting strings are sorted. If that serialization reaches an ancestor outside the member, the cycle crosses an unordered boundary and valuehash throws a clear TypeError. A cheap exact key for that case would require full graph canonization; isoequal can still decide equality when no key is required.

Serialization is proportional to the emitted representation in ordinary use, but that representation can be large: an exact key is linear-sized, and repeated unshared unfoldings can repeat content. Use valueHash when a fixed-width, probabilistic key is the right tradeoff. Sorting a collection of m encoded members adds O(m log m) comparisons.

API

valueKey(value: unknown): string
valueHash(value: unknown): number
createValueKey(options?: {
    isUnorderedCollection?: (value: object) => boolean
}): (value: unknown) => string
UNORDERED // === Symbol.for('unordered-collection')

valueKey returns an ASCII-safe, self-delimiting string. It is the exact API. valueHash applies standard 32-bit FNV-1a to that string and returns an unsigned number; it can collide, like every fixed-width digest.

Verification

npm test runs focused type/collision/cycle cases plus 4,500 seeded random golden-property checks. The generator covers shuffled Sets and Maps, sparse arrays, ordered cycles, NaN, -0, TypedArrays, ArrayBuffers, Dates, RegExps, boxed primitives, and null-prototype objects. Equal shuffled clones must share a key; unequal tree values must not.

npm run bench uses cyclebench to print the correctness matrix above and interleaved timing medians:

| workload | valueKey | valueHash | object-hash | hash-it | node-object-hash | hash-sum | |---|---:|---:|---:|---:|---:|---:| | small POJO | 0.55µs | 0.68µs | 10.54µs | 0.53µs | 0.95µs | 1.57µs | | 1k-node tree | 300.57µs | 379.88µs | 3,883.08µs | 309.32µs | 323.07µs | 1,117.50µs | | 10k-element array | 213.46µs | 318.20µs | 1,071.13µs | 271.54µs | 717.65µs | 2,037.00µs | | Set-heavy value | 90.10µs | 103.57µs | 760.35µs | 128.14µs | 82.03µs | 0.23µs (wrong) |

Measured on Node 24.13.1, Apple M5 Max. This was an interactive machine; background applications were not disabled. hash-sum's apparent Set win is a wrong-result path: the correctness probe shows that it ignores Set contents.

License

MIT © Xyra Sinclair