Skip to content

Releases: mauro-d/faircount

v1.1.0

Choose a tag to compare

@mauro-d mauro-d released this 16 Aug 20:17

Counting from a CommonJS project no longer needs a dynamic import: the package
answers require() as well, on the versions of Node that load ESM from it.

Added

  • A CommonJS project can require('faircount'). There is no CommonJS build: the
    exports map answers the require condition with the same ESM file, which Node
    reads from require() on the versions below.

Changed

  • Requires Node ^20.19.0 || >=22.12.0, the versions that load ESM from
    require() without a flag. A single floor would have promised 22.0 through
    22.11, where the feature sits behind a flag. Below the range import still
    works, but it is no longer covered.

Fixed

  • A fractional seed is refused instead of seeding the run of its floor: 1.5
    and 1 produced the same sequence, while the README and the types promised an
    integer.

The README now compares faircount with HyperLogLog, says that two estimators
never combine into one count, and gives what the sample weighs in bytes.

v1.0.0

Choose a tag to compare

@mauro-d mauro-d released this 11 Aug 19:57

The estimator is an object you create and keep: every counting function takes it
first, so one count can be saved, resumed, read at any point, and carried across
several sources.

Breaking

  • Requires Node 20 or newer.
  • estimateDistinct(estimator, source, options). epsilon, delta,
    expectedSize, seed and random belong to new CVM() alone; the options of
    the counting functions cover only how values reach it.
  • Sources are split by kind: estimateDistinctSync for an iterable you already
    hold, estimateDistinct for an async iterable or a Readable. Each refuses
    the other's sources and names the one to call, and the synchronous one takes no
    signal.
  • keyFn's parameter follows the element type of the source, so over an Order[]
    a wrong field is a compile error where it used to be any. TypeScript only:
    the runtime is unchanged.
  • DistinctEstimateStream is replaced by createEstimatorSink(estimator, options).
    The count is read from the estimator, so the sink has no result().
  • expectedSize is required and must be at least 1. A snapshot written by 0.4
    without it carries 0 and no longer restores.

Fixed

  • An abort lands while the source is waiting, not only between values.
  • A signal already aborted before the call rejects with ABORT_ERR on a
    Readable source too, instead of Node 24's ERR_STREAM_UNABLE_TO_PIPE.
  • createRandom throws with CVM_INVALID_OPTION on a bad seed, the one error
    that carried no code.

The hand-written types are now typechecked as a consumer compiles them, in CI and
before publishing.

v0.4.0

Choose a tag to compare

@mauro-d mauro-d released this 27 Jul 21:46

toJSON hands over an estimator's state as a plain object and the static
fromJSON rebuilds one from it, so a long count survives a restart. Both APIs
now take an AbortSignal. Every error the library raises about its own use
carries a code (CVM_INVALID_OPTION, CVM_INVALID_SOURCE,
CVM_INVALID_SNAPSHOT, CVM_UNSERIALIZABLE_VALUE), while an aborted signal
follows Node with ABORT_ERR.

keyFn is now typed to the values a Set dedups and JSON returns unchanged:
a string, number, boolean or null. TypeScript users passing an optional
field will see an error where a missing value used to be counted silently as
one distinct value.

v0.3.1

Choose a tag to compare

@mauro-d mauro-d released this 27 Jul 21:32

Fixes an out-of-memory crash in estimateDistinct when the source is a Readable
stream. A Readable that hands over one value per read, Readable.from included,
left the loop holding one pending callback per value until the heap ran out. The
promise API now pipes those sources instead of iterating them.

Only estimateDistinct with a Readable was affected. The stream API, the core
CVM, and array or async-iterable sources were not.

v0.3.0

Choose a tag to compare

@mauro-d mauro-d released this 07 Jul 10:37

On skewed streams the delete branch churns on the same hot keys, and V8
keeps deleted entries in the Set's bucket chains until the table is
rebuilt, so lookups walk ever-longer chains. This copies the set once
holes pile up past max(|X|, 1024): membership, order and randomness are
untouched, so same-seed results are bit-identical to 0.2.0.

The bench gains a skewed scenario and a below-threshold one, and the
README a "stream shape" table that also says where the estimator buys
nothing over a plain Set. npm run bench reproduces the numbers.

v0.2.0

Choose a tag to compare

@mauro-d mauro-d released this 04 Jul 21:13
  • add() now follows Algorithm 3's branching exactly (insert with probability
    p, remove otherwise) instead of an equivalent reformulation, cutting hash
    operations in half per element. Up to ~50% faster while the sample stays
    exact, ~7% faster once sub-sampling engages.
  • Benchmark output and README tables now label results "faircount" instead of
    "CVM": the numbers measure this implementation, not the abstract algorithm.

v0.1.1

Choose a tag to compare

@mauro-d mauro-d released this 03 Jul 07:07

Clarifies seed parameter description.

v0.1.0

Choose a tag to compare

@mauro-d mauro-d released this 02 Jul 20:37

First public release.

  • Estimates the number of distinct values (F0) in a stream with bounded memory and (ε, δ) guarantees.
  • Implements the total, unbiased CVM variant (Karayel, Watt, Khu, Meel & Tan, ITP 2025, Algorithm 3): it never fails, and the estimate's expected value is exactly the true count.
  • APIs: estimateDistinct (promise), DistinctEstimateStream (Writable sink), CVM (pure core), plus computeThreshold and createRandom.
  • ESM-only, zero runtime dependencies, hand-written TypeScript types, Node ≥ 18.