Skip to content

Commit cf8f994

Browse files
nicktrnericallam
andauthored
Feature: io.random() (#716)
* Add io.random * Add changeset --------- Co-authored-by: Eric Allam <eric@trigger.dev>
1 parent 2ae5178 commit cf8f994

8 files changed

Lines changed: 194 additions & 1 deletion

File tree

.changeset/good-dolphins-jam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
---
4+
5+
Add `io.random()` which wraps `Math.random()` in a Task with helpful options.

docs/_snippets/random-example.mdx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
```typescript Random example
2+
client.defineJob({
3+
id: "random-job",
4+
name: "Random Job",
5+
version: "0.0.1",
6+
trigger: eventTrigger({
7+
name: "example.event",
8+
}),
9+
run: async (payload, io, ctx) => {
10+
// generate random numbers
11+
const small = await io.random("random-small");
12+
const large = await io.random("random-large", {
13+
min: 10,
14+
max: 200,
15+
round: true
16+
});
17+
18+
await io.logger.info(`${small} is smaller than ${large}`);
19+
},
20+
});
21+
```

docs/mint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@
335335
"sdk/io/logger",
336336
"sdk/io/sendevent",
337337
"sdk/io/backgroundfetch",
338+
"sdk/io/random",
338339
"sdk/io/try",
339340
"sdk/io/registerinterval",
340341
"sdk/io/unregisterinterval",

docs/sdk/io/overview.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ If you want to send an event from outside a run (e.g. just from your backend) yo
4040

4141
`io.backgroundFetch()` allows you to fetch data from a URL that can take longer that the serverless timeout. The actual `fetch` request is performed on the Trigger.dev platform, and the response is sent back to you. An example use case is fetching data from a slow API, like some AI endpoints.
4242

43+
### [random()](/sdk/io/random)
44+
45+
`io.random()` is identical to `Math.random()` when called without options but ensures your random numbers are not regenerated on resume or retry. It will return a pseudo-random floating-point number between optional `min` (default: 0, inclusive) and `max` (default: 1, exclusive). Can optionally `round` to the nearest integer.
46+
4347
### [try()](/sdk/io/try)
4448

4549
`io.try()` allows you to run Tasks and catch any errors that are thrown, it's similar to a normal `try/catch` block but works with [io.runTask()](/sdk/io/runtask).

docs/sdk/io/random.mdx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
title: "io.random()"
3+
sidebarTitle: "random()"
4+
description: "`io.random()` is identical to `Math.random()` when called without options but ensures your random numbers are not regenerated on resume or retry. It will return a pseudo-random floating-point number between optional `min` (default: 0, inclusive) and `max` (default: 1, exclusive). Can optionally `round` to the nearest integer."
5+
---
6+
7+
## Parameters
8+
9+
<Snippet file="stable-key-param.mdx" />
10+
<ResponseField name="min" type="number" default="0" required>
11+
Sets the lower bound (inclusive). Can't be higher than `max`.
12+
</ResponseField>
13+
<ResponseField name="max" type="number" default="1" required>
14+
Sets the upper bound (exclusive). Can't be lower than `min`.
15+
</ResponseField>
16+
<ResponseField name="round" type="boolean" default="false" required>
17+
Controls rounding to the nearest integer. Any `max` integer will become inclusive when enabled. Rounding with floating-point bounds may cause unexpected skew and boundary inclusivity.
18+
</ResponseField>
19+
20+
## Returns
21+
22+
A `Promise` that resolves with a pseudo-random number. Always resolves to an integer when rounding is enabled.
23+
24+
<RequestExample>
25+
<Snippet file="random-example.mdx" />
26+
</RequestExample>

packages/trigger-sdk/src/io.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,90 @@ export class IO {
232232
});
233233
}
234234

235+
/** `io.random()` is identical to `Math.random()` when called without options but ensures your random numbers are not regenerated on resume or retry. It will return a pseudo-random floating-point number between optional `min` (default: 0, inclusive) and `max` (default: 1, exclusive). Can optionally `round` to the nearest integer.
236+
* @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information.
237+
* @param min Sets the lower bound (inclusive). Can't be higher than `max`.
238+
* @param max Sets the upper bound (exclusive). Can't be lower than `min`.
239+
* @param round Controls rounding to the nearest integer. Any `max` integer will become inclusive when enabled. Rounding with floating-point bounds may cause unexpected skew and boundary inclusivity.
240+
*/
241+
async random(
242+
cacheKey: string | any[],
243+
{
244+
min = 0,
245+
max = 1,
246+
round = false,
247+
}: {
248+
min?: number;
249+
max?: number;
250+
round?: boolean;
251+
} = {}
252+
) {
253+
return await this.runTask(
254+
cacheKey,
255+
async (task) => {
256+
if (min > max) {
257+
throw new Error(
258+
`Lower bound can't be higher than upper bound - min: ${min}, max: ${max}`
259+
);
260+
}
261+
262+
if (min === max) {
263+
await this.logger.warn(
264+
`Lower and upper bounds are identical. The return value is not random and will always be: ${min}`
265+
);
266+
}
267+
268+
const withinBounds = (max - min) * Math.random() + min;
269+
270+
if (!round) {
271+
return withinBounds;
272+
}
273+
274+
if (!Number.isInteger(min) || !Number.isInteger(max)) {
275+
await this.logger.warn(
276+
"Rounding enabled with floating-point bounds. This may cause unexpected skew and boundary inclusivity."
277+
);
278+
}
279+
280+
const rounded = Math.round(withinBounds);
281+
282+
return rounded;
283+
},
284+
{
285+
name: "random",
286+
icon: "dice-5-filled",
287+
params: { min, max, round },
288+
properties: [
289+
...(min === 0
290+
? []
291+
: [
292+
{
293+
label: "min",
294+
text: String(min),
295+
},
296+
]),
297+
...(max === 1
298+
? []
299+
: [
300+
{
301+
label: "max",
302+
text: String(max),
303+
},
304+
]),
305+
...(round === false
306+
? []
307+
: [
308+
{
309+
label: "round",
310+
text: String(round),
311+
},
312+
]),
313+
],
314+
style: { style: "minimal" },
315+
}
316+
);
317+
}
318+
235319
/** `io.wait()` waits for the specified amount of time before continuing the Job. Delays work even if you're on a serverless platform with timeouts, or if your server goes down. They utilize [resumability](https://trigger.dev/docs/documentation/concepts/resumability) to ensure that the Run can be resumed after the delay.
236320
* @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information.
237321
* @param seconds The number of seconds to wait. This can be very long, serverless timeouts are not an issue.
@@ -975,7 +1059,7 @@ export class IO {
9751059
*/
9761060
brb = this.yield.bind(this);
9771061

978-
/** `io.try()` allows you to run Tasks and catch any errors that are thrown, it's similar to a normal `try/catch` block but works with [io.runTask()](/sdk/io/runtask).
1062+
/** `io.try()` allows you to run Tasks and catch any errors that are thrown, it's similar to a normal `try/catch` block but works with [io.runTask()](https://trigger.dev/docs/sdk/io/runtask).
9791063
* A regular `try/catch` block on its own won't work as expected with Tasks. Internally `runTask()` throws some special errors to control flow execution. This is necessary to deal with resumability, serverless timeouts, and retrying Tasks.
9801064
* @param tryCallback The code you wish to run
9811065
* @param catchCallback Thhis will be called if the Task fails. The callback receives the error

references/job-catalog/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"misconfigured": "nodemon --watch src/misconfigured.ts -r tsconfig-paths/register -r dotenv/config src/misconfigured.ts",
3030
"auto-yield": "nodemon --watch src/auto-yield.ts -r tsconfig-paths/register -r dotenv/config src/auto-yield.ts",
3131
"cli-example": "nodemon --watch src/cli-example.ts -r tsconfig-paths/register -r dotenv/config src/cli-example.ts",
32+
"random": "nodemon --watch src/random.ts -r tsconfig-paths/register -r dotenv/config src/random.ts",
3233
"invoke": "nodemon --watch src/invoke.ts -r tsconfig-paths/register -r dotenv/config src/invoke.ts",
3334
"dev:trigger": "trigger-cli dev --port 8080"
3435
},
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { createExpressServer } from "@trigger.dev/express";
2+
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
3+
4+
export const client = new TriggerClient({
5+
id: "job-catalog",
6+
apiKey: process.env["TRIGGER_API_KEY"],
7+
apiUrl: process.env["TRIGGER_API_URL"],
8+
verbose: false,
9+
ioLogLocalEnabled: true,
10+
});
11+
12+
client.defineJob({
13+
id: "random-example",
14+
name: "Random Example",
15+
version: "1.0.0",
16+
enabled: true,
17+
trigger: eventTrigger({
18+
name: "random.example",
19+
}),
20+
run: async (payload, io, ctx) => {
21+
// just like Math.random() but wrapped in a Task
22+
await io.random("random-native");
23+
24+
// set lower and upper bounds - defaults to 0, 1 respectively
25+
await io.random("random-min-max", { min: 10, max: 20 });
26+
27+
// set lower bound only (inclusive)
28+
await io.random("random-min", { min: 0.5 });
29+
30+
// set upper bound only (exclusive)
31+
await io.random("random-max", { max: 100 });
32+
33+
// round to the nearest integer
34+
await io.random("random-round", { min: 100, max: 1000, round: true });
35+
36+
// rounding with floating-point bounds results in a warning
37+
// this example will unexpectedly (but correctly!) output 1 or 2, skewing towards 2
38+
await io.random("random-round-float", { min: 0.9, max: 2.5, round: true });
39+
40+
// negative values work just fine
41+
await io.random("random-negative", { min: -100, max: -50 });
42+
43+
// identical lower and upper bounds result in a warning
44+
await io.random("random-warn-bounds", { min: 10, max: 10 });
45+
46+
// invalid ranges will fail
47+
await io.random("random-error", { min: 10, max: 5 });
48+
},
49+
});
50+
51+
createExpressServer(client);

0 commit comments

Comments
 (0)