@sdxc/random 0.0.0-pre.1 → 2026.10.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +87 -39
  2. package/package.json +11 -2
package/README.md CHANGED
@@ -4,31 +4,31 @@ Seeded and system random streams with integer, float, pick and shuffle draws, an
4
4
 
5
5
  ## Installation
6
6
 
7
- ```bash
7
+ ```sh
8
8
  npm add @sdxc/random
9
9
  ```
10
10
 
11
- The main entry point has no dependencies. `@sdxc/random/schema` exports `RANDOM_STATE_SCHEMA`, a [`remix/data-schema`](https://www.npmjs.com/package/remix) schema; install `remix` to use it.
11
+ The main entry point has no dependencies. The `@sdxc/random/schema` entry point needs [`remix`](https://www.npmjs.com/package/remix) for `remix/data-schema`.
12
12
 
13
13
  ## Usage
14
14
 
15
- ### Rolling From A Seed
15
+ ### Roll from a seed
16
16
 
17
17
  ```typescript
18
18
  import { createRandom } from "@sdxc/random";
19
19
 
20
20
  let dice = createRandom("battle-42");
21
21
 
22
- dice.int(1, 6); // the same six numbers, in the same order, on every run
22
+ dice.int(1, 6); // the same values, in the same order, on every run
23
23
  dice.float(0.85, 1);
24
24
  dice.bool(0.1);
25
25
  dice.pick(["rock", "paper", "scissors"]);
26
26
  dice.shuffle([1, 2, 3, 4, 5]);
27
27
  ```
28
28
 
29
- ### Accepting Randomness In Your Own API
29
+ ### Accept randomness in your own API
30
30
 
31
- 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`.
31
+ Type the parameter as `Random` and default it to `systemRandom()`. Production passes nothing; a test passes a seeded stream instead of stubbing `Math.random`.
32
32
 
33
33
  ```typescript
34
34
  import type { Random } from "@sdxc/random";
@@ -39,13 +39,13 @@ function shouldSample(rate: number, random: Random = systemRandom()): boolean {
39
39
  return random.bool(rate);
40
40
  }
41
41
 
42
- shouldSample(0.1); // crypto-backed
42
+ shouldSample(0.1); // Web Crypto backed
43
43
  shouldSample(0.1, createRandom("sampler-test")); // reproducible
44
44
  ```
45
45
 
46
- ### Saving And Resuming A Stream
46
+ ### Save and resume a stream
47
47
 
48
- `state()` is plain JSON. `restoreRandom` continues from the next draw the original would have taken.
48
+ `state()` is plain JSON, and `restoreRandom` continues with the draw the original would have taken next.
49
49
 
50
50
  ```typescript
51
51
  import { createRandom, restoreRandom } from "@sdxc/random";
@@ -59,9 +59,9 @@ let resumed = restoreRandom(JSON.parse(saved));
59
59
  resumed.int(1, 100) === encounters.int(1, 100); // true
60
60
  ```
61
61
 
62
- ### Independent Streams From One Seed
62
+ ### Split one seed into independent streams
63
63
 
64
- `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.
64
+ `derive(label)` opens a stream seeded from the parent's seed and the label, so extra draws in one part of a program leave the values of another part unchanged.
65
65
 
66
66
  ```typescript
67
67
  import { createRandom, systemSeed } from "@sdxc/random";
@@ -76,64 +76,108 @@ let loot = session.derive("loot");
76
76
 
77
77
  ### `createRandom(seed: Seed): SeededRandom`
78
78
 
79
- 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.
79
+ Opens 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.
80
80
 
81
81
  ### `restoreRandom(state: RandomState): SeededRandom`
82
82
 
83
- Rebuild a stream from a `state()` snapshot, positioned exactly where the snapshot was taken.
83
+ Rebuilds a stream from a `state()` snapshot, positioned exactly where the snapshot was taken.
84
84
 
85
85
  ### `systemRandom(): Random`
86
86
 
87
- 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.
87
+ An unseeded stream drawing from `crypto.getRandomValues`, for jitter, sampling and tie-breakers. It has no seed or state, so it never replays. Generate keys and secrets with Web Crypto directly.
88
88
 
89
89
  ### `systemSeed(): number`
90
90
 
91
- A fresh 32-bit seed from `crypto.getRandomValues`. Log it, and the run replays by passing it back to `createRandom`.
91
+ A fresh 32-bit seed from `crypto.getRandomValues`. Log it, and passing it back to `createRandom` replays the run.
92
92
 
93
- ### `RANDOM_STATE_SCHEMA` from `@sdxc/random/schema`
93
+ ### `RANDOM_STATE_SCHEMA: Schema<unknown, RandomState>`
94
94
 
95
- 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.
95
+ From `@sdxc/random/schema`. A `remix/data-schema` schema for a `RandomState` read from storage: the seed is a string or number, and each of the four words is an unsigned 32-bit integer.
96
96
 
97
- ### `Random`
97
+ ### Types
98
+
99
+ #### `Random`
98
100
 
99
101
  The draws every stream offers:
100
102
 
101
103
  - `next()`: the raw draw, in `[0, 1)`
102
- - `int(min, max)`: an integer with both ends included; throws `RangeError` on non-integer bounds or `max < min`
104
+ - `int(min, max)`: an integer with both ends included; throws `RangeError` when a bound is not a safe integer or `max < min`
103
105
  - `float(min = 0, max = 1)`: a number in `[min, max)`
104
106
  - `bool(chance = 0.5)`: `true` with probability `chance`
105
107
  - `pick(items)`: one element, each equally likely; throws `RangeError` on an empty list
106
- - `shuffle(items)`: a shuffled copy, leaving the input untouched
108
+ - `shuffle(items)`: a shuffled copy, leaving the input in its order
109
+
110
+ #### `SeededRandom`
111
+
112
+ A `Random` plus `seed`, `derive(label: string): SeededRandom` and `state(): RandomState`.
107
113
 
108
- ### `SeededRandom`
114
+ #### `Seed`
109
115
 
110
- A `Random` with `seed`, `derive(label)` and `state()`.
116
+ `number | string`.
111
117
 
112
- ### `Seed` and `RandomState`
118
+ #### `RandomState`
113
119
 
114
- `Seed` is `number | string`. `RandomState` is `{ seed, words }`, where `words` holds the generator's four unsigned 32-bit words.
120
+ `{ seed: Seed; words: [number, number, number, number] }`, the seed and the generator's four unsigned 32-bit words.
115
121
 
116
- ## Pattern: A Reproducible Fuzz Run
122
+ ## Pattern: A Reproducible Fuzz Test
117
123
 
118
- Draw a fresh seed per run unless one is supplied, and print it, so a failing run replays from that number.
124
+ Draw a fresh seed per run unless one is supplied, and put it in the suite name, so a failure reports the number that replays it.
119
125
 
120
126
  ```typescript
121
127
  import { createRandom, systemSeed } from "@sdxc/random";
128
+ import { describe, expect, test } from "vitest";
129
+
130
+ import { parseCron } from "./cron.js";
122
131
 
123
132
  let seed = Number(process.env.FUZZ_SEED) || systemSeed();
124
- console.info(`fuzz seed: ${seed}`);
125
-
126
- let random = createRandom(seed);
127
- let cases = Array.from({ length: 1000 }, () => ({
128
- minute: random.int(0, 59),
129
- hour: random.int(0, 23),
130
- weekday: random.pick(["MON", "TUE", "WED", "THU", "FRI"]),
131
- }));
133
+
134
+ describe(`cron parsing (FUZZ_SEED=${seed})`, () => {
135
+ test("every generated expression parses", () => {
136
+ let random = createRandom(seed);
137
+
138
+ for (let index = 0; index < 1000; index++) {
139
+ let minute = random.int(0, 59);
140
+ let hour = random.int(0, 23);
141
+ let weekday = random.pick(["MON", "TUE", "WED", "THU", "FRI"]);
142
+ expect(parseCron(`${minute} ${hour} * * ${weekday}`).success).toBe(true);
143
+ }
144
+ });
145
+ });
146
+ ```
147
+
148
+ ## Pattern: Stable Procedural Generation
149
+
150
+ Give each feature of a generated world its own derived stream. Adding a draw to the terrain generator later keeps every existing world's loot exactly as it was.
151
+
152
+ ```typescript
153
+ import type { Random } from "@sdxc/random";
154
+
155
+ import { createRandom } from "@sdxc/random";
156
+
157
+ function terrain(random: Random): string[] {
158
+ return Array.from({ length: 8 }, () => random.pick(["plains", "forest", "hills", "lake"]));
159
+ }
160
+
161
+ function loot(random: Random): { gold: number; items: string[] } {
162
+ let gold = random.int(10, 100);
163
+ let items = random.shuffle(["sword", "shield", "potion", "map"]).slice(0, random.int(1, 3));
164
+ return { gold, items };
165
+ }
166
+
167
+ function generateWorld(seed: string) {
168
+ let world = createRandom(seed);
169
+ return {
170
+ terrain: terrain(world.derive("terrain")),
171
+ loot: loot(world.derive("loot")),
172
+ };
173
+ }
174
+
175
+ generateWorld("world-7"); // identical output for "world-7" on every machine
132
176
  ```
133
177
 
134
178
  ## Pattern: Persisting A Simulation
135
179
 
136
- Store each stream's state next to the world it shaped, and validate it on load, since a save file is untrusted input.
180
+ Store the stream's state next to the data it shaped, and validate it on load, since a save file is untrusted input.
137
181
 
138
182
  ```typescript
139
183
  import * as s from "remix/data-schema";
@@ -141,7 +185,7 @@ import * as s from "remix/data-schema";
141
185
  import { createRandom, restoreRandom } from "@sdxc/random";
142
186
  import { RANDOM_STATE_SCHEMA } from "@sdxc/random/schema";
143
187
 
144
- const SAVE_SCHEMA = s.object({ turn: s.number(), random: RANDOM_STATE_SCHEMA });
188
+ let saveSchema = s.object({ turn: s.number(), random: RANDOM_STATE_SCHEMA });
145
189
 
146
190
  let random = createRandom("campaign");
147
191
  let turn = 0;
@@ -150,14 +194,18 @@ function save(): string {
150
194
  return JSON.stringify({ turn, random: random.state() });
151
195
  }
152
196
 
153
- function load(text: string) {
154
- let parsed = s.parseSafe(SAVE_SCHEMA, JSON.parse(text));
155
- if (!parsed.success) throw new Error("Corrupt save file");
197
+ function load(text: string): boolean {
198
+ let parsed = s.parseSafe(saveSchema, JSON.parse(text));
199
+ if (!parsed.success) return false;
200
+
156
201
  turn = parsed.value.turn;
157
202
  random = restoreRandom(parsed.value.random);
203
+ return true;
158
204
  }
159
205
  ```
160
206
 
207
+ A save that fails the schema leaves the running simulation untouched, and `load` answers `false`.
208
+
161
209
  ## Versioning
162
210
 
163
211
  Releases are dated rather than semantic. A version is the UTC date it was published, written `YYYY.M.D`, so `2026.9.4` is the release from 4 September 2026. At most one release goes out per day.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdxc/random",
3
- "version": "0.0.0-pre.1",
3
+ "version": "2026.10.13",
4
4
  "description": "Seeded and system random streams with integer, float, pick and shuffle draws, and resumable state",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  "optional": true
17
17
  }
18
18
  },
19
- "gitHead": "47e70835fe3943a13cf37f07c3dfb8203aba1c35",
19
+ "gitHead": "5ff758a57e35480a8db7abd24f1d9527b81bfb51",
20
20
  "publishConfig": {
21
21
  "access": "public"
22
22
  },
@@ -24,5 +24,14 @@
24
24
  "type": "git",
25
25
  "url": "git+https://github.com/sergiodxa/monorepo.git",
26
26
  "directory": "packages/random"
27
+ },
28
+ "homepage": "https://sdxc.sergiodxa.com/api/random",
29
+ "author": {
30
+ "name": "Sergio Xalambrí",
31
+ "url": "https://sergiodxa.com"
32
+ },
33
+ "funding": {
34
+ "type": "github",
35
+ "url": "https://github.com/sponsors/sergiodxa"
27
36
  }
28
37
  }