Skip to content

Commit 7e0bf33

Browse files
authored
Effect API Rate Limit, Cache Control & Remove Experimental (#809)
* Update cusror rules * Add rate limit support * Per call effect cache config via context.cache * Remove experimental for createEffect
1 parent 6217dc7 commit 7e0bf33

File tree

15 files changed

+1071
-349
lines changed

15 files changed

+1071
-349
lines changed

.cursor/worktrees.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
{
2-
"setup-worktree": ["cd scenarios/test_codegen && pnpm install"]
2+
"setup-worktree": [
3+
"cd scenarios/test_codegen && pnpm install",
4+
"pnpm codegen",
5+
"pnpm rescript",
6+
"pnpm ts:build"
7+
]
38
}

codegenerator/cli/npm/envio/index.d.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export type {
44
effectContext as EffectContext,
55
effectArgs as EffectArgs,
66
effectOptions as EffectOptions,
7+
rateLimitDuration as RateLimitDuration,
8+
rateLimit as RateLimit,
79
blockEvent as BlockEvent,
810
onBlockArgs as OnBlockArgs,
911
onBlockOptions as OnBlockOptions,
@@ -13,6 +15,7 @@ export type { EffectCaller } from "./src/Types.ts";
1315
import type {
1416
effect as Effect,
1517
effectArgs as EffectArgs,
18+
rateLimit as RateLimit,
1619
} from "./src/Envio.gen.ts";
1720

1821
import { schema as bigDecimalSchema } from "./src/bindings/BigDecimal.gen.ts";
@@ -73,6 +76,33 @@ type Flatten<T> = T extends object
7376
// })
7477
// The behaviour is inspired by Sury code:
7578
// https://github.com/DZakh/sury/blob/551f8ee32c1af95320936d00c086e5fb337f59fa/packages/sury/src/S.d.ts#L344C1-L355C50
79+
export function createEffect<
80+
IS,
81+
OS,
82+
I = UnknownToOutput<IS>,
83+
O = UnknownToOutput<OS>,
84+
// A hack to enforce that the inferred return type
85+
// matches the output schema type
86+
R extends O = O
87+
>(
88+
options: {
89+
/** The name of the effect. Used for logging and debugging. */
90+
readonly name: string;
91+
/** The input schema of the effect. */
92+
readonly input: IS;
93+
/** The output schema of the effect. */
94+
readonly output: OS;
95+
/** Rate limit for the effect. Set to false to disable or provide {calls: number, per: "second" | "minute"} to enable. */
96+
readonly rateLimit: RateLimit;
97+
/** Whether the effect should be cached. */
98+
readonly cache?: boolean;
99+
},
100+
handler: (args: EffectArgs<I>) => Promise<R>
101+
): Effect<I, O>;
102+
103+
/**
104+
* @deprecated Use createEffect instead. The only difference is that rateLimit option becomes required. Set it to false to keep the same behaviour.
105+
*/
76106
export function experimental_createEffect<
77107
IS,
78108
OS,
@@ -89,6 +119,8 @@ export function experimental_createEffect<
89119
readonly input: IS;
90120
/** The output schema of the effect. */
91121
readonly output: OS;
122+
/** Rate limit for the effect. Set to false to disable or provide {calls: number, per: "second" | "minute"} to enable. */
123+
readonly rateLimit?: RateLimit;
92124
/** Whether the effect should be cached. */
93125
readonly cache?: boolean;
94126
},

codegenerator/cli/npm/envio/src/Envio.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,34 @@ export type logger = $$logger;
2929

3030
export type effect<input,output> = $$effect<input,output>;
3131

32+
export type rateLimitDuration = "second" | "minute" | number;
33+
34+
export type rateLimit =
35+
false
36+
| { readonly calls: number; readonly per: rateLimitDuration };
37+
38+
export type experimental_effectOptions<input,output> = {
39+
/** The name of the effect. Used for logging and debugging. */
40+
readonly name: string;
41+
/** The input schema of the effect. */
42+
readonly input: RescriptSchema_S_t<input>;
43+
/** The output schema of the effect. */
44+
readonly output: RescriptSchema_S_t<output>;
45+
/** Rate limit for the effect. Set to false to disable or provide {calls: number, per: "second" | "minute"} to enable. */
46+
readonly rateLimit?: rateLimit;
47+
/** Whether the effect should be cached. */
48+
readonly cache?: boolean
49+
};
50+
3251
export type effectOptions<input,output> = {
3352
/** The name of the effect. Used for logging and debugging. */
3453
readonly name: string;
3554
/** The input schema of the effect. */
3655
readonly input: RescriptSchema_S_t<input>;
3756
/** The output schema of the effect. */
3857
readonly output: RescriptSchema_S_t<output>;
58+
/** Rate limit for the effect. Set to false to disable or provide {calls: number, per: "second" | "minute"} to enable. */
59+
readonly rateLimit: rateLimit;
3960
/** Whether the effect should be cached. */
4061
readonly cache?: boolean
4162
};

codegenerator/cli/npm/envio/src/Envio.res

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,28 @@ type logger = {
3232
@@warning("-30") // Duplicated type names (input)
3333
@genType.import(("./Types.ts", "Effect"))
3434
type rec effect<'input, 'output>
35+
@genType @unboxed
36+
and rateLimitDuration =
37+
| @as("second") Second
38+
| @as("minute") Minute
39+
| Milliseconds(int)
40+
@genType @unboxed
41+
and rateLimit =
42+
| @as(false) Disable
43+
| Enable({calls: int, per: rateLimitDuration})
44+
@genType
45+
and experimental_effectOptions<'input, 'output> = {
46+
/** The name of the effect. Used for logging and debugging. */
47+
name: string,
48+
/** The input schema of the effect. */
49+
input: S.t<'input>,
50+
/** The output schema of the effect. */
51+
output: S.t<'output>,
52+
/** Rate limit for the effect. Set to false to disable or provide {calls: number, per: "second" | "minute"} to enable. */
53+
rateLimit?: rateLimit,
54+
/** Whether the effect should be cached. */
55+
cache?: bool,
56+
}
3557
@genType
3658
and effectOptions<'input, 'output> = {
3759
/** The name of the effect. Used for logging and debugging. */
@@ -40,13 +62,16 @@ and effectOptions<'input, 'output> = {
4062
input: S.t<'input>,
4163
/** The output schema of the effect. */
4264
output: S.t<'output>,
65+
/** Rate limit for the effect. Set to false to disable or provide {calls: number, per: "second" | "minute"} to enable. */
66+
rateLimit: rateLimit,
4367
/** Whether the effect should be cached. */
4468
cache?: bool,
4569
}
4670
@genType.import(("./Types.ts", "EffectContext"))
4771
and effectContext = {
4872
log: logger,
4973
effect: 'input 'output. (effect<'input, 'output>, 'input) => promise<'output>,
74+
mutable cache: bool,
5075
}
5176
@genType
5277
and effectArgs<'input> = {
@@ -55,12 +80,23 @@ and effectArgs<'input> = {
5580
}
5681
@@warning("+30")
5782

58-
let experimental_createEffect = (
83+
let durationToMs = (duration: rateLimitDuration) =>
84+
switch duration {
85+
| Second => 1000
86+
| Minute => 60000
87+
| Milliseconds(ms) => ms
88+
}
89+
90+
let createEffect = (
5991
options: effectOptions<'input, 'output>,
6092
handler: effectArgs<'input> => promise<'output>,
6193
) => {
6294
let outputSchema =
6395
S.schema(_ => options.output)->(Utils.magic: S.t<S.t<'output>> => S.t<Internal.effectOutput>)
96+
let itemSchema = S.schema((s): Internal.effectCacheItem => {
97+
id: s.matches(S.string),
98+
output: s.matches(outputSchema),
99+
})
64100
{
65101
name: options.name,
66102
handler: handler->(
@@ -78,20 +114,48 @@ let experimental_createEffect = (
78114
Utils.magic: S.t<S.t<'input>> => S.t<Internal.effectInput>
79115
),
80116
output: outputSchema,
81-
cache: switch options.cache {
82-
| Some(true) =>
83-
let itemSchema = S.schema((s): Internal.effectCacheItem => {
84-
id: s.matches(S.string),
85-
output: s.matches(outputSchema),
86-
})
117+
storageMeta: {
118+
table: Internal.makeCacheTable(~effectName=options.name),
119+
outputSchema,
120+
itemSchema,
121+
},
122+
defaultShouldCache: switch options.cache {
123+
| Some(true) => true
124+
| _ => false
125+
},
126+
rateLimit: switch options.rateLimit {
127+
| Disable => None
128+
| Enable({calls, per}) =>
87129
Some({
88-
table: Internal.makeCacheTable(~effectName=options.name),
89-
outputSchema,
90-
itemSchema,
130+
callsPerDuration: calls,
131+
durationMs: per->durationToMs,
132+
availableCalls: calls,
133+
windowStartTime: Js.Date.now(),
134+
queueCount: 0,
135+
nextWindowPromise: None,
91136
})
92-
| None
93-
| Some(false) =>
94-
None
95137
},
96138
}->(Utils.magic: Internal.effect => effect<'input, 'output>)
97139
}
140+
141+
@deprecated(
142+
"Use createEffect instead. The only difference is that rateLimit option becomes required. Set it to false to keep the same behaviour."
143+
)
144+
let experimental_createEffect = (
145+
options: experimental_effectOptions<'input, 'output>,
146+
handler: effectArgs<'input> => promise<'output>,
147+
) => {
148+
createEffect(
149+
{
150+
name: options.name,
151+
input: options.input,
152+
output: options.output,
153+
rateLimit: switch options.rateLimit {
154+
| Some(rateLimit) => rateLimit
155+
| None => Disable
156+
},
157+
cache: ?options.cache,
158+
},
159+
handler,
160+
)
161+
}

codegenerator/cli/npm/envio/src/Internal.res

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -281,27 +281,37 @@ let makeEnumConfig = (~name, ~variants) => {
281281

282282
type effectInput
283283
type effectOutput
284-
type effectContext
284+
type effectContext = private {mutable cache: bool}
285285
type effectArgs = {
286286
input: effectInput,
287287
context: effectContext,
288288
cacheKey: string,
289289
}
290290
type effectCacheItem = {id: string, output: effectOutput}
291-
type effectCacheMeta = {
291+
type effectCacheStorageMeta = {
292292
itemSchema: S.t<effectCacheItem>,
293293
outputSchema: S.t<effectOutput>,
294294
table: Table.table,
295295
}
296+
type rateLimitState = {
297+
callsPerDuration: int,
298+
durationMs: int,
299+
mutable availableCalls: int,
300+
mutable windowStartTime: float,
301+
mutable queueCount: int,
302+
mutable nextWindowPromise: option<promise<unit>>,
303+
}
296304
type effect = {
297305
name: string,
298306
handler: effectArgs => promise<effectOutput>,
299-
cache: option<effectCacheMeta>,
307+
storageMeta: effectCacheStorageMeta,
308+
defaultShouldCache: bool,
300309
output: S.t<effectOutput>,
301310
input: S.t<effectInput>,
302311
// The number of functions that are currently running.
303312
mutable activeCallsCount: int,
304313
mutable prevCallStartTimerRef: Hrtime.timeRef,
314+
rateLimit: option<rateLimitState>,
305315
}
306316
let cacheTablePrefix = "envio_effect_"
307317
let cacheOutputSchema = S.json(~validate=false)->(Utils.magic: S.t<Js.Json.t> => S.t<effectOutput>)

codegenerator/cli/npm/envio/src/PgStorage.res

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -789,13 +789,7 @@ let make = (
789789
~items: array<Internal.effectCacheItem>,
790790
~initialize: bool,
791791
) => {
792-
let {table, itemSchema} = switch effect.cache {
793-
| Some(cacheMeta) => cacheMeta
794-
| None =>
795-
Js.Exn.raiseError(
796-
`Failed to set effect cache for "${effect.name}". Effect has no cache enabled.`,
797-
)
798-
}
792+
let {table, itemSchema} = effect.storageMeta
799793

800794
if initialize {
801795
let _ =

codegenerator/cli/npm/envio/src/Prometheus.res

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,18 @@ module EffectCacheInvalidationsCount = {
646646
}
647647
}
648648

649+
module EffectQueueCount = {
650+
let gauge = SafeGauge.makeOrThrow(
651+
~name="envio_effect_queue_count",
652+
~help="The number of effect calls waiting in the rate limit queue.",
653+
~labelSchema=effectLabelsSchema,
654+
)
655+
656+
let set = (~count, ~effectName) => {
657+
gauge->SafeGauge.handleInt(~labels=effectName, ~value=count)
658+
}
659+
}
660+
649661
module StorageLoad = {
650662
let operationLabelsSchema = S.object(s => s.field("operation", S.string))
651663

codegenerator/cli/npm/envio/src/Types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export type EffectContext = {
4444
* Define a new Effect using createEffect outside of the handler.
4545
*/
4646
readonly effect: EffectCaller;
47+
/**
48+
* Whether to cache the result of the effect. Defaults to the effect's cache configuration.
49+
* Set to false to disable caching for this specific call.
50+
*/
51+
cache: boolean;
4752
};
4853

4954
export type GenericContractRegister<Args> = (

0 commit comments

Comments
 (0)