sdxc

Type to search, or start from one of these:

[ Language & values ]

@sdxc/random

Seeded and system random streams with integer, float, pick and shuffle draws, and resumable state

npm add @sdxc/random
pnpm add @sdxc/random
yarn add @sdxc/random
bun add @sdxc/random
Used by
uptime, auth-saas

Seeded and system random streams with integer, float, pick and shuffle draws, and resumable state.

Installation

npm add @sdxc/random

The main entry point has no dependencies. @sdxc/random/schema exports RANDOM_STATE_SCHEMA, a remix/data-schema schema; install remix to use it.

Usage

Rolling From A Seed

import { createRandom } from "@sdxc/random";

let dice = createRandom("battle-42");

dice.int(1, 6); // the same six numbers, in the same order, on every run
dice.float(0.85, 1);
dice.bool(0.1);
dice.pick(["rock", "paper", "scissors"]);
dice.shuffle([1, 2, 3, 4, 5]);

Accepting Randomness In Your Own API

Declare the parameter as Random and default it to systemRandom(). Production passes nothing, and a test passes a seeded stream instead of stubbing Math.random.

import type { Random } from "@sdxc/random";

import { createRandom, systemRandom } from "@sdxc/random";

function shouldSample(rate: number, random: Random = systemRandom()): boolean {
	return random.bool(rate);
}

shouldSample(0.1); // crypto-backed
shouldSample(0.1, createRandom("sampler-test")); // reproducible

Saving And Resuming A Stream

state() is plain JSON. restoreRandom continues from the next draw the original would have taken.

import { createRandom, restoreRandom } from "@sdxc/random";

let encounters = createRandom("world-7");
encounters.int(1, 100);

let saved = JSON.stringify(encounters.state());
let resumed = restoreRandom(JSON.parse(saved));

resumed.int(1, 100) === encounters.int(1, 100); // true

Independent Streams From One Seed

derive(label) opens a stream seeded from the parent's seed and the label, so extra draws in one part of a program never shift the values another part sees.

import { createRandom, systemSeed } from "@sdxc/random";

let session = createRandom(systemSeed());

let battle = session.derive("battle");
let loot = session.derive("loot");

API

createRandom(seed: Seed): SeededRandom

Open a stream on a seed. The same seed produces the same values in the same order on any machine; 42 and "42" name the same stream.

restoreRandom(state: RandomState): SeededRandom

Rebuild a stream from a state() snapshot, positioned exactly where the snapshot was taken.

systemRandom(): Random

An unseeded stream drawing from crypto.getRandomValues, for jitter, sampling and short tie-breakers. It has no seed and no state, so it cannot be replayed. Use Web Crypto directly, or a token library, for keys and secrets.

systemSeed(): number

A fresh 32-bit seed from crypto.getRandomValues. Log it, and the run replays by passing it back to createRandom.

RANDOM_STATE_SCHEMA from @sdxc/random/schema

A remix/data-schema schema for a RandomState read from storage. It checks the seed's type and that each of the four words is an unsigned 32-bit integer.

Random

The draws every stream offers:

  • next(): the raw draw, in [0, 1)

  • int(min, max): an integer with both ends included; throws RangeError on non-integer bounds or max < min

  • float(min = 0, max = 1): a number in [min, max)

  • bool(chance = 0.5): true with probability chance

  • pick(items): one element, each equally likely; throws RangeError on an empty list

  • shuffle(items): a shuffled copy, leaving the input untouched

SeededRandom

A Random with seed, derive(label) and state().

Seed and RandomState

Seed is number | string. RandomState is { seed, words }, where words holds the generator's four unsigned 32-bit words.

Pattern: A Reproducible Fuzz Run

Draw a fresh seed per run unless one is supplied, and name the suite after it, so a failure reports the number that replays it.

import { createRandom, systemSeed } from "@sdxc/random";
import { describe, expect, test } from "vitest";

const SEED = Number(process.env.FUZZ_SEED) || systemSeed();

describe(`cron parsing (FUZZ_SEED=${SEED})`, () => {
	test("every generated expression parses", () => {
		let random = createRandom(SEED);

		for (let index = 0; index < 1000; index++) {
			let minute = random.int(0, 59);
			let hour = random.int(0, 23);
			let weekday = random.pick(["MON", "TUE", "WED", "THU", "FRI"]);
			expect(parseCron(`${minute} ${hour} * * ${weekday}`).success).toBe(true);
		}
	});
});

parseCron is the function under test.

Pattern: Persisting A Simulation

Store each stream's state next to the world it shaped, and validate it on load, since a save file is untrusted input.

import * as s from "remix/data-schema";

import { createRandom, restoreRandom } from "@sdxc/random";
import { RANDOM_STATE_SCHEMA } from "@sdxc/random/schema";

const SAVE_SCHEMA = s.object({ turn: s.number(), random: RANDOM_STATE_SCHEMA });

let random = createRandom("campaign");
let turn = 0;

function save(): string {
	return JSON.stringify({ turn, random: random.state() });
}

function load(text: string): boolean {
	let parsed = s.parseSafe(SAVE_SCHEMA, JSON.parse(text));
	if (!parsed.success) return false;

	turn = parsed.value.turn;
	random = restoreRandom(parsed.value.random);
	return true;
}

A save that fails the schema leaves the running game as it was, and load answers false so the caller can say the file was unreadable.

Written by Sergio Xalambrí. Follow @sergiodxa for new packages, or sponsor the work.