[ Language & values ]
@sdxc/random
Seeded and system random streams with integer, float, pick and shuffle draws, and resumable state
- Used by
- uptime, auth-saas
- Source
- packages/random
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; throwsRangeErroron non-integer bounds ormax < minfloat(min = 0, max = 1): a number in[min, max)bool(chance = 0.5):truewith probabilitychancepick(items): one element, each equally likely; throwsRangeErroron an empty listshuffle(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.