Production-grade infrastructure for Adobe ExtendScript.
|
ESON ESB64 ESARR ESSTR ESCHARS ESHTTP ESTIMER ESRAND ESUUID ESENV ESPATH ESFS ESHASH ESLOG |
ESPACK ESMIN ESABI VectorIPC ESTC ESDB COMTool ESsemble |
Also from the same team: ArcFit.dev, deterministic arc warp for Illustrator.
- Why ESRAND?
- Features
- Which artifact should I use?
- Get the Release
- Installation
- Quick Start
- API Reference
- Validation
- Performance
- Security Model
- Compatibility
- Engine quirks that shaped the design
- Development
- Repository layout
- Known limitations
- Credits
- License
ExtendScript exposes Math.random(), but it does not provide a reproducible random infrastructure layer: no seed input, no portable state snapshot, no versioned sequence contract, no unbiased bounded-integer helper, and no jump/split primitive for deterministic independent streams.
The native generator's output resolution is also host-specific. On Adobe Illustrator 30.6.0 / ExtendScript 4.5.6, a live 20,000-call probe observed:
- 14,965 distinct outputs and 5,035 collisions;
- 0 out-of-range outputs;
- minimum positive spacing consistent with
1 / 32768; - 0 / 20,000 violations of
x * 32768 === floor(x * 32768).
A preceding independent 20,000-call probe observed the same minimum spacing and 14,920 distinct values. Those measurements support only the host/version-scoped statement that Illustrator 30.6.0 / ExtendScript 4.5.6 emitted values on a 1/32768 grid during these probes.
ESRAND makes the sequence explicit instead:
xoshiro128**1.1 with four 32-bit state words;- canonical tagged seed serialization hashed with MurmurHash3 x86_128;
- 32-bit and 53-bit
[0,1)lanes; - unbiased inclusive integers;
- snapshot/restore;
- official
2^64/2^96jumps; - deterministic bulk-fill APIs.
- Frozen deterministic contract v1 — algorithm version 1 and seed version 1 are embedded in snapshots and pinned by a SHA-256 whole-surface fingerprint.
- 128-bit generator state —
xoshiro128**1.1 has period2^128 - 1; the all-zero state is rejected. - Canonical MurmurHash3 x86_128 seeding — number, UTF-16 string, and finite
number[]domains are type-tagged and differential-tested againstmurmurhash3js. - Two fraction lanes —
random32()consumes one word;random()combines 27 + 26 high bits from two words into a 53-bit binary64 fraction. - Unbiased inclusive integers — rejection sampling removes modulo bias for widths up to
2^32. - Fisher-Yates collection semantics —
shuffle(),shuffled(), andsample()never use a randomized sort comparator. - O(count) sparse sampling scratch — small samples from large collections avoid copying the entire source while preserving partial-Fisher-Yates draw semantics.
- Official stream partitioning —
jump(),longJump(), andsplit()use the published xoshiro jump polynomials. - Bulk generation — fill arrays with uint32, 32/53-bit fractions, floats, booleans, Bernoulli draws, unbiased integers, and bytes without repeated facade overhead.
- Versioned snapshots — persisted state carries algorithm ID, algorithm version, seed version, and all four state words.
- Persistent-engine reload preservation — the vendor build retains an already-installed compatible
$.global.ESRANDinstead of silently resetting its default stream. - ESTC release output — official ExtendScript assets are built through the shared ExtendScript toolchain, then conservatively minified and live parsed.
- ESTIMER-only live timing — ESRAND runtime code never reads
$.hiresTimerdirectly.
| Artifact | Size | Installs | Best for |
|---|---|---|---|
ESRAND.min.jsx |
27,485 B | $.global.ESRAND, preserving a compatible facade |
Recommended standalone release build |
vendor-esrand.min.js |
27,485 B | $.global.ESRAND, preserving a compatible facade |
Recommended persistent/shared-engine build |
ESRAND.jsx |
37,102 B | $.global.ESRAND, preserving a compatible facade |
Readable standalone debugging |
vendor-esrand.js |
37,102 B | $.global.ESRAND, preserving a compatible facade |
Readable persistent-engine debugging |
ESRAND.facade.jsx |
37,189 B | Loader-free $.global.ESRAND activation |
ESPACK v2 library-composition input |
ESRAND.manifest.json |
50,878 B | Exact facade bytes, SHA-256, activation contract, provenance | Build-time dependency input for composed consumers such as ESUUID |
esrand-core.esm.mjs |
50,772 B | ESM exports | Node reference/tests/tooling |
Rule of thumb: use ESRAND.min.jsx for a self-contained script and vendor-esrand.min.js when multiple scripts share ESRAND through a persistent ExtendScript engine. Their runtime facade semantics are intentionally identical; the names distinguish packaging intent.
The official JSX release artifacts are ESTC-normalized builds. npm run build
also emits the ESPACK v2 facade/manifest pair. That sidecar is not loaded by
ESRAND at runtime; it lets a downstream composer resolve ESRAND by stable
identity/version, verify its exact facade hash, and activate it before a
consumer such as ESUUID. npm run build:release adds conservative
minification and final static portability gates.
The current stable release is ESRAND v0.2.0.
Release assets:
ESRAND.min.jsx— recommended standalone ExtendScript bundle;vendor-esrand.min.js— recommended persistent-engine vendor bundle;ESRAND.jsxandvendor-esrand.js— readable ESTC-normalized counterparts;ESRAND.facade.jsxandESRAND.manifest.json— ESPACK 0.5 manifest-v2 composition inputs;esrand-core.esm.mjs— Node/reference ESM build;esrand-v0.2.0.lock.json— tag, commit, determinism fingerprint, sizes, and per-artifact SHA-256;SHA256SUMS.txt— release-asset checksums.
For reproducible production use, pin the v0.2.0 tag or release asset rather than a mutable branch.
Download ESRAND.min.jsx from the release and load/include it:
#include "/absolute/path/to/ESRAND.min.jsx"
var rng = ESRAND.create("poster-v1");
var value = rng.uint32();The standalone artifact deliberately contains no #target illustrator; the runtime core has no Illustrator DOM dependency.
$.evalFile(File("/absolute/path/to/vendor-esrand.min.js"));
var rng = $.global.ESRAND.create("poster-v1");
var value = rng.uint32();Re-evaluating a compatible vendor build preserves the existing installed facade and its lazy default stream.
git clone https://github.com/thelabcorner/es-rand.git
cd es-rand
npm ci
npm run verifynpm run build produces the Node ESM reference artifact plus development JSX. Official production JSX is generated by the maintainer build:release pipeline through the shared ESTC/minification toolchain.
var rng = ESRAND.create("cover-art-2026");
var fastUnit = rng.random32(); // one generator word
var unit = rng.random(); // full 53-bit construction
var die = rng.int(1, 6); // inclusive, unbiased
var jitter = rng.float(-5, 5);
var enabled = rng.chance(0.25);
var order = ["ink", "type", "texture", "marks"];
rng.shuffle(order);
var picked = rng.sample(order, 2);
var bulk = [];
rng.fillUint32(bulk, 64);
var snapshot = rng.getState();
var next = rng.uint32();
var replay = ESRAND.fromState(snapshot);
var sameNext = replay.uint32(); // exactly equal to nextFor reproducible work, prefer an explicit ESRAND.create(seed) generator over the lazily auto-seeded module-level convenience methods.
The top-level ESRAND facade exposes:
| API | Behavior |
|---|---|
version() |
Package version. |
algorithm() |
Algorithm ID/version, seed version, state width, period, crypto flag. |
constants() |
Numeric/contract constants. |
capabilities() |
Runtime capability report. |
create(seed?) |
Independent generator; explicit seed is deterministic. |
fromState(snapshotOrWords) |
Restore from versioned snapshot or raw four-word expert state. |
reseed(seed?) |
Replace the lazy default generator. |
getState() / setState(...) |
Snapshot/restore the default generator. |
All scalar, collection, bulk, byte, and jump methods below are also exposed by the facade and forward to its lazy default generator.
| API | State use | Result |
|---|---|---|
uint32() |
1 word | unsigned 32-bit integer |
int32() |
1 word | signed 32-bit integer |
random32() |
1 word | [0,1), uint32 / 2^32 |
random() |
2 words | 53-bit binary64 fraction in [0,1) |
float32(min,max) |
1 word for unequal bounds | scaled random32() |
float(min,max) |
2 words for unequal bounds | scaled 53-bit random() |
int(min,max) |
1+ words when rejection is possible | unbiased inclusive safe integer |
bool() |
1 word | high-bit boolean |
chance(p) |
2 words | 53-bit Bernoulli draw |
Equal float bounds and singleton integer ranges return the sole value without consuming state.
| API | Behavior |
|---|---|
choice(values) |
Uniform member selection; empty input throws. |
sample(values,count) |
Without replacement; exact partial-Fisher-Yates semantics with sparse remapping. |
shuffle(array) |
In-place Fisher-Yates; returns the same array. |
shuffled(values) |
Copies, shuffles, returns the copy. |
Each fill method writes from index 0 and returns the supplied target.
fillUint32(target, count?)
fillRandom32(target, count?)
fillRandom(target, count?)
fillFloat32(target, min, max, count?)
fillFloat(target, min, max, count?)
fillBool(target, count?)
fillChance(target, probability, count?)
fillInt(target, min, max, count?)
fillBytes(target, count?)
When count is omitted, the target's existing length is used. Each bulk lane is contract-equivalent to repeated calls to its corresponding scalar operation.
| API | Behavior |
|---|---|
bytes(count) |
Numeric byte array; four little-endian bytes per generator word. |
fillBytes(target,count?) |
Bulk form of canonical byte emission. |
hex(byteCount) |
Lowercase hex of the same generated byte stream. |
| API | Behavior |
|---|---|
getState() |
Versioned snapshot. |
setState(snapshotOrWords) |
Restore this generator. |
clone() |
Independent generator at the identical current state. |
jump() |
Official 2^64 xoshiro jump. |
longJump() |
Official 2^96 xoshiro long jump. |
split() |
Return clone at old state; advance parent by one 2^64 jump. |
Snapshot schema:
{
algorithm: "xoshiro128**",
algorithmVersion: 1,
seedVersion: 1,
state: [s0, s1, s2, s3]
}Persist the complete snapshot rather than raw words when long-term compatibility matters.
Explicit seeds support finite number, string, and finite number[].
Canonical serialization is type-tagged and little-endian, then hashed with MurmurHash3 x86_128 using seed 0x45535231 ("ESR1"):
- numeric:
NUM1 || uint32(seed); - string:
STR1 || u32(length) || UTF-16LE code units; - array:
ARR1 || u32(length) || u32(element)....
Numeric seeds are intentionally uint32-coerced, so 1 and 4294967297 identify the same numeric seed. Type domains stay distinct: 123, "123", and [123] do not alias.
The complete normative contract is in docs/DETERMINISM.md.
| Check | Command | Result |
|---|---|---|
| Strict TypeScript | npm run typecheck |
pass |
| Unit/reference vectors | npm test |
28,277 assertions passed |
| MurmurHash3 differential | npm run murmur-differential |
2,372 / 2,372 x86_128 vectors match murmurhash3js 3.0.1 |
| Frozen sequence fingerprint | npm run determinism-fingerprint |
79f898bd…2e5b3b |
| Deterministic/property fuzz | npm run fuzz |
58,334 checks passed |
| Statistical + seed-avalanche smoke | npm run statistical |
8,499 checks passed; 8,192-case avalanche mean 64.057 / 128 bits |
| Release ES3 static checks | npm run build:release |
4 / 4 ExtendScript artifacts pass ESTC conservative parser checks |
| Release live compile-only parse | npm run release:live-parse |
4 / 4 pass on Illustrator 30.6.0 / ExtendScript 4.5.6 |
| Unminified vendor parity | release gate | 11 / 11 exact Node ↔ Illustrator groups |
| Minified vendor parity | npm run live-verify:minified |
11 / 11 exact Node ↔ Illustrator groups |
The frozen whole-surface fingerprint covers 512 seeded cases across raw words, 53-bit fractions, bounded integers, shuffled/sampled collections, bytes/hex, snapshots, and jump states.
The statistical suite is a deterministic gross-defect detector and avalanche smoke test. It is not a cryptographic certification.
All ExtendScript timing is performed through ESTIMER. ESRAND runtime code never reads $.hiresTimer directly.
Environment:
Adobe Illustrator 30.6.0
ExtendScript 4.5.6
Windows
ESTIMER engine timing lane
3 independent benchmark invocations
5 warmups + 9 measured samples per lane per invocation
reported value = median of per-run ESTIMER medians
0 rejected samples in the reported lanes
| Lane | Median µs/op | Run-median range µs/op |
|---|---|---|
native Math.random() |
0.348 | 0.316–0.535 |
create(number) single |
15.0 | 15–25 |
create(string16) single |
46.0 | 46–47 |
create(string64) single |
112.0 | 112–113 |
uint32() |
1.535 | 1.534–1.587 |
random32() |
1.593 | 1.573–1.745 |
random() 53-bit |
2.333 | 2.317–3.450 |
float32(-2.5,9.25) |
2.521 | 2.507–2.542 |
float(-2.5,9.25) |
3.189 | 3.187–3.360 |
int(0,999) |
2.518 | 2.514–2.554 |
bool() |
1.540 | 1.527–1.540 |
chance(0.25) |
2.773 | 2.737–2.865 |
choice(16) |
2.596 | 2.595–2.597 |
| Lane | Median µs/op | Run-median range µs/op |
|---|---|---|
shuffle(64) core |
80.08 | 79.36–127.20 |
shuffled(64) |
96.96 | 95.96–101.32 |
sample(64,8) |
17.40 | 17.40–17.44 |
sample(1000,8) |
19.12 | 19.00–19.16 |
sample(10000,8) |
47.80 | 47.60–66.00 |
bytes(64) |
26.99 | 26.19–27.47 |
hex(64) |
27.36 | 24.81–28.34 |
fillBytes(64) |
21.44 | 21.33–21.85 |
fillUint32(64) |
48.36 | 46.32–73.15 |
fillRandom32(64) |
48.40 | 47.99–78.20 |
fillRandom(64) |
98.67 | 95.20–124.41 |
fillInt(64,-512..511) |
52.87 | 50.89–54.70 |
The native scalar remains faster. ESRAND spends additional time to provide explicit deterministic state, 32/53-bit mappings, unbiased bounded integers, snapshots, and jump-separated streams.
The complete structured benchmark record is in evidence/illustrator-30.6.0-live-benchmark-v0.1.0.json.
ESRAND is a non-cryptographic deterministic PRNG.
The runtime does not evaluate strings, load native code, perform network I/O, write files, or replace Math.random. The persistent vendor artifact may install $.global.ESRAND; a compatible installed facade is retained on reload.
Automatic seeding mixes wall-clock milliseconds, two independent native Math.random() draws when available, and an engine-local counter, then feeds that array through seed contract v1. This is convenience entropy, not a CSPRNG.
Do not use ESRAND for:
- passwords;
- authentication/session tokens;
- cryptographic keys;
- security nonces;
- adversarial lotteries or financial fairness;
- any decision requiring unpredictable randomness against an attacker.
See SECURITY.md for vulnerability reporting.
| Target | Status |
|---|---|
| Adobe Illustrator 30.6.0 / ExtendScript 4.5.6 | Live verified: 11/11 exact behavioral groups; 4/4 release artifacts live parse |
| Node 20 / 22 portable verification | CI matrix configured; local Node gate passes |
| ES3-compatible ExtendScript syntax | Official release artifacts pass ESTC's conservative ES3 parser/profile |
| Other Adobe ExtendScript hosts | Core is host-agnostic by design; not yet live-verified |
| Cryptographic use | Unsupported by design |
Official release JSX deliberately contains no #target illustrator; the runtime core has no Illustrator DOM dependency.
On Adobe Illustrator 30.6.0 / ExtendScript 4.5.6, the final 20,000-call probe produced 14,965 unique values and 5,035 collisions, with zero violations of x * 32768 === floor(x * 32768).
That observation is version-scoped evidence, not a claim about every ExtendScript host.
The live host probe reports no Math.imul. ESRAND performs exact low-32-bit multiplication with 16-bit limbs and uses inlined fixed-constant equivalents in hot Murmur lanes.
The early raw generated JSX failed at the parser when modern tooling materialized those names in unsafe syntactic positions. Public calls such as rng.float(...) remain ergonomic; the JSX facade constructs the reserved keys safely, and ESTC rewrites emitted representations to the portable profile.
An exported esbuild/ESTC entry can synthesize module-namespace helpers around APIs such as Object.defineProperty, Object.getOwnPropertyDescriptor, and Object.getOwnPropertyNames. A live Illustrator 30.6.0 / ExtendScript 4.5.6 V2 probe demonstrated that the descriptor path is not available there, so syntactically valid output can still be runtime-invalid.
ESRAND therefore makes its ExtendScript entry side-effect-only: it installs or preserves $.global.ESRAND directly and exports no entry binding for esbuild to wrap. The build and release-build paths additionally reject any emitted artifact containing those descriptor-helper names. ESTC remains the normalization/type/lint boundary; plain esbuild remains only on the separate Node ESM surface.
A same-version vendor reload intentionally preserves $.global.ESRAND. The live verifier first clears the installed global before loading the artifact under test, then separately verifies same-build reload preservation. This prevents a stale resident facade from masquerading as a successful verification run.
Portable gate:
npm ci
npm run verifyIllustrator development checks:
npm run live-verify
npm run live-benchmark:aggregate
npm run probe:math-randomMaintainer release gate inside the shared Scripts workspace:
npm run release:gate
npm run release:manifestrelease:gate performs the portable verification suite, ESTC production build, conservative ExtendScript minification, static ES3 checks, four compile-only live parses, unminified behavioral parity, and minified behavioral parity.
npm publish is wired through the same gate via prepublishOnly; this is intentional. The publish lifecycle rebuilds and revalidates the same ESTC-owned artifacts that ship to ExtendScript.
src/
generator.ts xoshiro state machine + scalar/collection/bulk APIs
seed.ts canonical seed v1 serialization and optimized seed paths
murmur128.ts ES3-safe MurmurHash3 x86_128
index.ts modern/default facade
jsx-entry.ts ES3-safe JSX facade boundary
types.ts public TypeScript contracts
u32.ts exact 32-bit helpers
docs/
DETERMINISM.md normative v1 sequence contract
RESEARCH.md source selection and evidence notes
tests/
esrand-test.mjs
murmur-differential.mjs
determinism-fingerprint.mjs
fuzz.mjs
statistical.mjs
esrand-live-verify.mjs
esrand-v2-live-probe.jsx
minified-live-verify.mjs
live-benchmark.mjs
live-benchmark-aggregate.mjs
math-random-live.mjs
evidence/
measured live-host JSON records
tooling/
release-build.mjs
release-live-parse.mjs
release-manifest.mjs
- ESRAND is not cryptographic.
- Explicit numeric seeds are uint32-coerced by contract.
- Inclusive
int(min,max)range width is limited to2^32, even when endpoints are larger safe integers. - Other Adobe ExtendScript hosts have not yet received the same live behavioral certification as Illustrator 30.6.0.
- ExtendScript JSX builds require the shared sibling ESTC workspace used by the ES* projects. A standalone clone can run the portable Node verification, but canonical JSX emission intentionally has no raw-esbuild fallback.
- Statistical smoke tests detect gross defects; they do not replace a dedicated statistical-test battery or cryptographic analysis.
ESRAND builds on:
- David Blackman and Sebastiano Vigna — xoshiro/xoroshiro family and the published
xoshiro128**1.1 jump constants: https://prng.di.unimi.it/; - Austin Appleby / SMHasher — public-domain MurmurHash3 x86_128 reference: https://github.com/aappleby/smhasher;
- Docs for Adobe / Types-for-Adobe — ExtendScript and host typing/reference work: https://github.com/docsforadobe/Types-for-Adobe;
- ESTIMER — the sibling timing layer used for every reported live benchmark.
GPL-3.0-or-later. See LICENSE.
ESRAND: deterministic random infrastructure for ExtendScript-era JavaScript.