Skip to content

Commit cc15dec

Browse files
committed
feat: Leverage local rate limiting if available in environment
1 parent 991ed89 commit cc15dec

11 files changed

+486
-1
lines changed

analyze/index.ts

+112
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import initWasm, {
44
is_valid_email,
55
type EmailValidationConfig,
66
} from "./wasm/arcjet_analyze_js_req.js";
7+
import { instantiate } from "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.js";
78

89
export { type EmailValidationConfig };
910

@@ -163,3 +164,114 @@ export async function detectBot(
163164
};
164165
}
165166
}
167+
168+
function nowInSeconds(): number {
169+
return Math.floor(Date.now() / 1000);
170+
}
171+
172+
class Cache<T> {
173+
expires: Map<string, number>;
174+
data: Map<string, T>;
175+
176+
constructor() {
177+
this.expires = new Map();
178+
this.data = new Map();
179+
}
180+
181+
get(key: string) {
182+
const ttl = this.ttl(key);
183+
if (ttl > 0) {
184+
return this.data.get(key);
185+
} else {
186+
// Cleanup if expired
187+
this.expires.delete(key);
188+
this.data.delete(key);
189+
}
190+
}
191+
192+
set(key: string, value: T, ttl: number) {
193+
this.expires.set(key, nowInSeconds() + ttl);
194+
this.data.set(key, value);
195+
}
196+
197+
ttl(key: string): number {
198+
const now = nowInSeconds();
199+
const expiresAt = this.expires.get(key) ?? now;
200+
return expiresAt - now;
201+
}
202+
}
203+
204+
const rateLimitCache = new Cache<string>();
205+
206+
export async function fixedWindow(
207+
config: {
208+
key: string;
209+
characteristics?: string[];
210+
max: number;
211+
window: number;
212+
},
213+
request: unknown,
214+
): Promise<{
215+
allowed: boolean;
216+
max: number;
217+
remaining: number;
218+
reset: number;
219+
}> {
220+
const configJson = JSON.stringify(config);
221+
const requestJson = JSON.stringify(request);
222+
223+
const abc = await instantiate(
224+
async function (path: string) {
225+
if (process.env["NEXT_RUNTIME"] === "edge") {
226+
if (path === "arcjet_analyze_bindings_rate_limit.component.core.wasm") {
227+
const mod = await import(
228+
// @ts-expect-error
229+
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core.wasm?module"
230+
);
231+
return mod.default;
232+
}
233+
if (
234+
path === "arcjet_analyze_bindings_rate_limit.component.core2.wasm"
235+
) {
236+
const mod = await import(
237+
// @ts-expect-error
238+
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core2.wasm?module"
239+
);
240+
return mod.default;
241+
}
242+
if (
243+
path === "arcjet_analyze_bindings_rate_limit.component.core3.wasm"
244+
) {
245+
const mod = await import(
246+
// @ts-expect-error
247+
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core3.wasm?module"
248+
);
249+
return mod.default;
250+
}
251+
} else {
252+
// const { wasm } = await import("./wasm/arcjet.wasm.js");
253+
// wasmModule = await WebAssembly.compile(await wasm());
254+
return Promise.reject("TODO");
255+
}
256+
},
257+
{
258+
"arcjet:rate-limit/storage": {
259+
get(key) {
260+
return rateLimitCache.get(key);
261+
},
262+
set(key, value, ttl) {
263+
rateLimitCache.set(key, value, ttl);
264+
},
265+
},
266+
"arcjet:rate-limit/time": {
267+
now() {
268+
return nowInSeconds();
269+
},
270+
},
271+
},
272+
);
273+
274+
const resultJson = abc.fixedWindow(configJson, requestJson);
275+
// TODO: Try/catch and Validate
276+
return JSON.parse(resultJson);
277+
}

analyze/rollup.config.js

+27
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,33 @@ export default createConfig(import.meta.url, {
1919
external: true,
2020
};
2121
}
22+
if (
23+
source ===
24+
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core.wasm?module"
25+
) {
26+
return {
27+
id: "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core.wasm?module",
28+
external: true,
29+
};
30+
}
31+
if (
32+
source ===
33+
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core2.wasm?module"
34+
) {
35+
return {
36+
id: "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core2.wasm?module",
37+
external: true,
38+
};
39+
}
40+
if (
41+
source ===
42+
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core3.wasm?module"
43+
) {
44+
return {
45+
id: "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core3.wasm?module",
46+
external: true,
47+
};
48+
}
2249
// TODO: Generation of this file can be handled via rollup plugin so we
2350
// wouldn't need to externalize here
2451
if (source === "./wasm/arcjet.wasm.js") {
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ArcjetRateLimitStorage } from './interfaces/arcjet-rate-limit-storage.js';
2+
import { ArcjetRateLimitTime } from './interfaces/arcjet-rate-limit-time.js';
3+
export interface ImportObject {
4+
'arcjet:rate-limit/storage': typeof ArcjetRateLimitStorage,
5+
'arcjet:rate-limit/time': typeof ArcjetRateLimitTime,
6+
}
7+
export interface Root {
8+
tokenBucket(config: string, request: string): string,
9+
fixedWindow(config: string, request: string): string,
10+
slidingWindow(config: string, request: string): string,
11+
}
12+
13+
/**
14+
* Instantiates this component with the provided imports and
15+
* returns a map of all the exports of the component.
16+
*
17+
* This function is intended to be similar to the
18+
* `WebAssembly.instantiate` function. The second `imports`
19+
* argument is the "import object" for wasm, except here it
20+
* uses component-model-layer types instead of core wasm
21+
* integers/numbers/etc.
22+
*
23+
* The first argument to this function, `getCoreModule`, is
24+
* used to compile core wasm modules within the component.
25+
* Components are composed of core wasm modules and this callback
26+
* will be invoked per core wasm module. The caller of this
27+
* function is responsible for reading the core wasm module
28+
* identified by `path` and returning its compiled
29+
* `WebAssembly.Module` object. This would use `compileStreaming`
30+
* on the web, for example.
31+
*/
32+
export function instantiate(
33+
getCoreModule: (path: string) => Promise<WebAssembly.Module>,
34+
imports: ImportObject,
35+
instantiateCore?: (module: WebAssembly.Module, imports: Record<string, any>) => Promise<WebAssembly.Instance>
36+
): Promise<Root>;
37+

0 commit comments

Comments
 (0)