Release Helper Tools 0.1.0

This commit is contained in:
2026-09-01 02:33:45 +02:00
commit 5fffd48642
78 changed files with 11635 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
import {
asUint8Array,
assertBoundedBytes,
assertBoundedItems,
assertPositiveSafeInteger,
type BytesLike,
} from "./limits";
export type RandomSeed = string | number | bigint | BytesLike;
export interface SeededRandom {
nextUint32(): number;
nextFloat(): number;
integer(minimum: number, maximumExclusive: number): number;
bytes(length: number): Uint8Array<ArrayBuffer>;
shuffle<T>(values: readonly T[]): T[];
state(): readonly [number, number, number, number];
}
export function secureRandomBytes(
length: number,
cryptoProvider: Pick<Crypto, "getRandomValues"> = crypto,
maximumLength = 1024 * 1024,
): Uint8Array<ArrayBuffer> {
assertPositiveSafeInteger(maximumLength, "Maximum random byte length");
if (!Number.isSafeInteger(length) || length < 0)
throw new TypeError(
"Random byte length must be a non-negative safe integer",
);
if (length > maximumLength)
throw new RangeError(`Random byte length exceeds ${maximumLength}`);
const output = new Uint8Array(length);
for (let offset = 0; offset < output.length; offset += 65_536) {
cryptoProvider.getRandomValues(
output.subarray(offset, Math.min(output.length, offset + 65_536)),
);
}
return output;
}
export function secureRandomInt(
minimum: number,
maximumExclusive: number,
cryptoProvider: Pick<Crypto, "getRandomValues"> = crypto,
): number {
validateIntegerRange(minimum, maximumExclusive);
const range = maximumExclusive - minimum;
if (range > 0x1_0000_0000)
throw new RangeError("Secure random integer range cannot exceed 2^32");
const limit = Math.floor(0x1_0000_0000 / range) * range;
const sample = new Uint32Array(1);
do cryptoProvider.getRandomValues(sample);
while ((sample[0] ?? 0) >= limit);
return minimum + ((sample[0] ?? 0) % range);
}
export function createSeededRandom(seed: RandomSeed): SeededRandom {
const state = seedState(seed);
const nextUint32 = () => {
const result = Math.imul(rotl(Math.imul(state[1] ?? 0, 5), 7), 9) >>> 0;
const temporary = ((state[1] ?? 0) << 9) >>> 0;
state[2] = ((state[2] ?? 0) ^ (state[0] ?? 0)) >>> 0;
state[3] = ((state[3] ?? 0) ^ (state[1] ?? 0)) >>> 0;
state[1] = ((state[1] ?? 0) ^ (state[2] ?? 0)) >>> 0;
state[0] = ((state[0] ?? 0) ^ (state[3] ?? 0)) >>> 0;
state[2] = ((state[2] ?? 0) ^ temporary) >>> 0;
state[3] = rotl(state[3] ?? 0, 11);
return result;
};
const integer = (minimum: number, maximumExclusive: number) => {
validateIntegerRange(minimum, maximumExclusive);
const range = maximumExclusive - minimum;
if (range > 0x1_0000_0000)
throw new RangeError("Seeded integer range cannot exceed 2^32");
const limit = Math.floor(0x1_0000_0000 / range) * range;
let sample: number;
do sample = nextUint32();
while (sample >= limit);
return minimum + (sample % range);
};
return {
nextUint32,
nextFloat: () => nextUint32() / 0x1_0000_0000,
integer,
bytes(length) {
assertBoundedItems(length, 1024 * 1024, "Random byte length");
const output = new Uint8Array(length);
let word = 0;
for (let index = 0; index < length; index += 1) {
if (index % 4 === 0) word = nextUint32();
output[index] = word & 0xff;
word >>>= 8;
}
return output;
},
shuffle<T>(values: readonly T[]): T[] {
assertBoundedItems(values.length);
const output = [...values];
for (let index = output.length - 1; index > 0; index -= 1) {
const swap = integer(0, index + 1);
[output[index], output[swap]] = [output[swap]!, output[index]!];
}
return output;
},
state: () =>
[state[0] ?? 0, state[1] ?? 0, state[2] ?? 0, state[3] ?? 0] as const,
};
}
export function shuffleSeeded<T>(values: readonly T[], seed: RandomSeed): T[] {
return createSeededRandom(seed).shuffle(values);
}
function seedState(seed: RandomSeed): Uint32Array<ArrayBuffer> {
let bytes: Uint8Array<ArrayBufferLike>;
if (typeof seed === "string") bytes = new TextEncoder().encode(seed);
else if (typeof seed === "number" || typeof seed === "bigint")
bytes = new TextEncoder().encode(String(seed));
else bytes = assertBoundedBytes(seed, 1024 * 1024, "Seed byte length");
if (bytes.byteLength === 0) bytes = asUint8Array(new Uint8Array([0]));
const state = new Uint32Array(4);
let hash = 0x811c9dc5;
for (let lane = 0; lane < state.length; lane += 1) {
for (const byte of bytes) {
hash ^= byte + lane;
hash = Math.imul(hash, 0x01000193) >>> 0;
hash ^= hash >>> 13;
}
hash = Math.imul(hash ^ (hash >>> 16), 0x85ebca6b) >>> 0;
state[lane] = hash || 0x9e3779b9 ^ lane;
}
return state;
}
function rotl(value: number, count: number): number {
return ((value << count) | (value >>> (32 - count))) >>> 0;
}
function validateIntegerRange(minimum: number, maximumExclusive: number): void {
if (
!Number.isSafeInteger(minimum) ||
!Number.isSafeInteger(maximumExclusive) ||
maximumExclusive <= minimum
) {
throw new RangeError(
"Random integer bounds must be safe integers with maximum greater than minimum",
);
}
}