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

@hadron-memory/distri-machine

v0.1.0

Published

A small traffic-bucketing library for A/B testing: fraction-balanced or JsonLogic-filter assignment, with host-owned state.

Readme

@hadron-memory/distri-machine

A small traffic-bucketing library for A/B testing. You give it a subject and a running tally; it gives you back a bucket name. It does assignment — not measurement, persistence, or significance testing. Those stay with the host.

  • Fraction mode — split traffic toward target shares (e.g. 50/50), balanced exactly on live counts.
  • Filter mode — route by attributes with JsonLogic rules, first match wins.
  • One runtime dependency (json-logic-js), MIT, ships ESM + CJS + types.
npm install @hadron-memory/distri-machine

Fraction mode — A/B split

import { DistributionMachine } from '@hadron-memory/distri-machine';

const machine = new DistributionMachine({
  shuffle: true, // recommended in fraction mode (see "Randomization" below)
  buckets: [
    { name: 'control', target_fraction: 0.5 },
    { name: 'variant', target_fraction: 0.5 },
  ],
});

// The host owns this tally. Persist it; share it across instances if you can.
const distribution = new Map<string, number>();

const bucket = machine.check({ id: 'user-123' }, distribution);
// → 'control' or 'variant'; `distribution` is incremented for the winner.

Counts converge exactly to the targets — unlike hash-based assignment, which is only asymptotic and can skew badly on small/segmented cohorts. Uneven splits (0.7 / 0.3) and three-way splits work the same way.

Filter mode — route by attributes

const machine = new DistributionMachine({
  buckets: [
    {
      name: 'mexico-over-50',
      sort_index: 0, // most specific first
      rule: {
        and: [
          { '==': [{ var: 'country' }, 'MX'] },
          { '>': [{ var: 'age' }, 50] },
        ],
      },
    },
    { name: 'mexico', sort_index: 1, rule: { '==': [{ var: 'country' }, 'MX'] } },
    { name: 'everyone', sort_index: 2, rule: true }, // catch-all
  ],
});

machine.check({ id: '1', country: 'MX', age: 60 }, new Map()); // 'mexico-over-50'

Buckets are evaluated in ascending sort_index; the first rule that matches wins, so order specific rules before general ones. A rule may be a JsonLogic object or the JSON-encoded string you stored in a text field — both are accepted.

Don't mix modes in one machine. A machine is either fraction buckets or filter buckets. Mixing is undefined behavior by design. To do "50/50 within Mexico-over-50", compose two machines yourself: one gates eligibility, one splits.

Variables

Named constants are merged into the evaluation data under the reserved $vars key, so a subject property can never shadow them. Reference them with { "var": "$vars.<name>" }:

{
  name: 'eligible',
  rule: {
    and: [
      { '==': [{ var: 'country' }, { var: '$vars.country' }] },
      { '>':  [{ var: 'age' },     { var: '$vars.min_age' }] },
    ],
  },
  variables: [
    { name: 'country', type: 'string', value: 'MX' },
    { name: 'min_age', type: 'number', value: 50 },
  ],
}

Dates

JsonLogic has no date type. Pass dates as ISO 8601 strings (which compare correctly with < / > lexically) or as epoch numbers. Variable.type ('date') is metadata telling the host how to coerce; the library passes the value through unchanged.

regex operator

This library adds a regex operator to JsonLogic: { "regex": [<value>, <pattern>, <flags?>] } → boolean. A missing value or an invalid pattern yields false rather than throwing.

{ name: 'gmail', rule: { regex: [{ var: 'email' }, '@gmail\\.com$', 'i'] } }

Override map

A force-assign escape hatch checked before any bucket logic — for QA, demos, and escalations. Keyed by the subject's id (idKey, default "id"):

new DistributionMachine({
  buckets: [/* … */],
  overrides: { 'user-123': 'variant' }, // subjectId → bucketName
  idKey: 'id',
});

An overridden subject short-circuits and does not increment any count.

API

new DistributionMachine<T extends object>(options): DistributionMachine<T>

options: {
  buckets: Bucket[];
  shuffle?: boolean;                  // fraction mode: shuffle order each check
  overrides?: Record<string, string>; // subjectId → bucketName
  idKey?: string;                     // subject id property, default 'id'
  random?: () => number;              // injectable RNG (tests); default Math.random
}

machine.check(subject: T, distribution: Map<string, number>): string | null

check returns the winning bucket name and increments its count in distribution, or returns null if nothing matched. Configure a final catch-all bucket (target_fraction: 1 or rule: true) if you never want null.

A Bucket is either a fraction bucket or a filter bucket:

interface Bucket {
  name: string;
  sort_index?: number;                 // filter mode ordering (default 0)
  target_fraction?: number;            // fraction mode: target share 0–1
  rule?: JsonLogicRule | string;       // filter mode: JsonLogic (object or JSON string)
  variables?: Variable[];              // constants exposed under $vars
}

What the host owns

This library is pure on configuration and keeps no state of its own. Two things are explicitly your responsibility:

  1. The distribution map. You create, persist, and (ideally) share it. check mutates it in place. Two server instances with separate in-memory maps will drift slightly; back the map with something central if exactness across instances matters. Not solved in v1.

  2. Stickiness. check is not deterministic per subject — re-checking the same subject can return a different bucket as counts shift. Persist the first assignment and never re-check an already-assigned subject.

Randomization

Fraction mode is balanced (block-style) randomization, not independent per-subject coin flips. Each subject still has roughly its target odds of each variant, and balanced group sizes increase statistical power — this is the scheme clinical trials use to keep arms evenly sized. Use shuffle: true so no bucket gets systematic preference when several are at or below target.

If you need significance-grade independent sampling (assignment independence per subject), that would be a future hash-based mode — out of scope for v1.

License

MIT © Hadron Memory