Skip to content

Commit dbf1c4e

Browse files
committed
seed Math.random in workflow environment
1 parent 1f95da0 commit dbf1c4e

File tree

3 files changed

+50
-17
lines changed

3 files changed

+50
-17
lines changed

src/client/environment.test.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe("environment patching units", () => {
99
describe("patchMath", () => {
1010
it("should preserve all Math methods except random", () => {
1111
const originalMath = Math;
12-
const patchedMath = patchMath(originalMath);
12+
const patchedMath = patchMath(originalMath, "test-seed");
1313

1414
// Should preserve all other methods
1515
expect(patchedMath.abs).toBe(originalMath.abs);
@@ -19,20 +19,31 @@ describe("environment patching units", () => {
1919
expect(patchedMath.E).toBe(originalMath.E);
2020
});
2121

22-
it("should replace Math.random with function that throws", () => {
22+
it("should replace Math.random with deterministic seeded function", () => {
2323
const originalMath = Math;
24-
const patchedMath = patchMath(originalMath);
25-
26-
expect(() => patchedMath.random()).toThrow(
27-
"Math.random() isn't yet supported within workflows",
28-
);
24+
const patchedMath = patchMath(originalMath, "test-workflow-id");
25+
26+
// Should return a number between 0 and 1
27+
const random1 = patchedMath.random();
28+
expect(random1).toBeGreaterThanOrEqual(0);
29+
expect(random1).toBeLessThan(1);
30+
31+
// Should be deterministic - same seed produces same sequence
32+
const patchedMath2 = patchMath(originalMath, "test-workflow-id");
33+
const random2 = patchedMath2.random();
34+
expect(random2).toBe(random1);
35+
36+
// Different seed produces different sequence
37+
const patchedMath3 = patchMath(originalMath, "different-workflow-id");
38+
const random3 = patchedMath3.random();
39+
expect(random3).not.toBe(random1);
2940
});
3041

3142
it("should not mutate the original Math object", () => {
3243
const originalMath = Math;
3344
const originalRandom = Math.random;
3445

35-
patchMath(originalMath);
46+
patchMath(originalMath, "test-seed");
3647

3748
// Original Math should be unchanged
3849
expect(Math.random).toBe(originalRandom);

src/client/environment.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
11
type GenerationState = { now: number; latest: boolean };
22

3-
// Testable unit: patches Math object to restrict non-deterministic functions
4-
export function patchMath(math: typeof Math): typeof Math {
3+
// Simple hash function to convert a string to a 32-bit seed
4+
function hashString(str: string): number {
5+
let hash = 0;
6+
for (let i = 0; i < str.length; i++) {
7+
const char = str.charCodeAt(i);
8+
hash = (hash << 5) - hash + char;
9+
hash = hash & hash; // Convert to 32-bit integer
10+
}
11+
return hash >>> 0; // Ensure unsigned
12+
}
13+
14+
// Mulberry32 - a simple, fast seeded PRNG
15+
function createSeededRandom(seed: number): () => number {
16+
return function () {
17+
seed |= 0;
18+
seed = (seed + 0x6d2b79f5) | 0;
19+
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
20+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
21+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
22+
};
23+
}
24+
25+
// Testable unit: patches Math object to use seeded random
26+
export function patchMath(math: typeof Math, seed: string): typeof Math {
527
const patchedMath = Object.create(Object.getPrototypeOf(math));
628

729
// Copy all properties from original Math
@@ -14,10 +36,9 @@ export function patchMath(math: typeof Math): typeof Math {
1436
}
1537
}
1638

17-
// Override random to throw
18-
patchedMath.random = () => {
19-
throw new Error("Math.random() isn't yet supported within workflows");
20-
};
39+
// Override random to use seeded PRNG
40+
const seededRandom = createSeededRandom(hashString(seed));
41+
patchedMath.random = seededRandom;
2142

2243
return patchedMath;
2344
}
@@ -63,11 +84,12 @@ export function createDeterministicDate(
6384

6485
export function setupEnvironment(
6586
getGenerationState: () => GenerationState,
87+
workflowId: string,
6688
): void {
6789
const global = globalThis as Record<string, unknown>;
6890

69-
// Patch Math
70-
global.Math = patchMath(global.Math as typeof Math);
91+
// Patch Math with seeded random based on workflowId
92+
global.Math = patchMath(global.Math as typeof Math, workflowId);
7193

7294
// Patch Date
7395
const originalDate = global.Date as typeof Date;

src/client/workflowMutation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export function workflowMutation<ArgsValidator extends PropertyValidators>(
128128
Date.now(),
129129
workpoolOptions,
130130
);
131-
setupEnvironment(executor.getGenerationState.bind(executor));
131+
setupEnvironment(executor.getGenerationState.bind(executor), workflowId);
132132

133133
const handlerWorker = async (): Promise<WorkerResult> => {
134134
let runResult: RunResult;

0 commit comments

Comments
 (0)