Skip to content

Commit

Permalink
feat: Leverage local rate limiting if available in environment
Browse files Browse the repository at this point in the history
  • Loading branch information
blaine-arcjet committed Feb 8, 2024
1 parent 715ef15 commit adaaa9b
Show file tree
Hide file tree
Showing 17 changed files with 767 additions and 15 deletions.
154 changes: 154 additions & 0 deletions analyze/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import initWasm, {
is_valid_email,
type EmailValidationConfig,
} from "./wasm/arcjet_analyze_js_req.js";
import * as rateLimit from "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.js";
import type { Root as RateLimitAPI } from "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.js";

export { type EmailValidationConfig };

Expand Down Expand Up @@ -163,3 +165,155 @@ export async function detectBot(
};
}
}

function nowInSeconds(): number {
return Math.floor(Date.now() / 1000);
}

class Cache<T> {
expires: Map<string, number>;
data: Map<string, T>;

constructor() {
this.expires = new Map();
this.data = new Map();
}

get(key: string) {
const ttl = this.ttl(key);
if (ttl > 0) {
return this.data.get(key);
} else {
// Cleanup if expired
this.expires.delete(key);
this.data.delete(key);
}
}

set(key: string, value: T, expireAt: number) {
this.expires.set(key, expireAt);
this.data.set(key, value);
}

ttl(key: string): number {
const now = nowInSeconds();
const expiresAt = this.expires.get(key) ?? now;
return expiresAt - now;
}
}

const rateLimitCache = new Cache<string>();

const wasmCache = new Map<string, WebAssembly.Module>();

async function moduleFromPath(path: string): Promise<WebAssembly.Module> {
const cachedModule = wasmCache.get(path);
if (typeof cachedModule !== "undefined") {
return cachedModule;
}

if (process.env["NEXT_RUNTIME"] === "edge") {
if (path === "arcjet_analyze_bindings_rate_limit.component.core.wasm") {
const mod = await import(
// @ts-expect-error
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core.wasm?module"
);
wasmCache.set(path, mod.default);
return mod.default;
}
if (path === "arcjet_analyze_bindings_rate_limit.component.core2.wasm") {
const mod = await import(
// @ts-expect-error
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core2.wasm?module"
);
wasmCache.set(path, mod.default);
return mod.default;
}
if (path === "arcjet_analyze_bindings_rate_limit.component.core3.wasm") {
const mod = await import(
// @ts-expect-error
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core3.wasm?module"
);
wasmCache.set(path, mod.default);
return mod.default;
}
} else {
if (path === "arcjet_analyze_bindings_rate_limit.component.core.wasm") {
const { wasm } = await import(
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core.js"
);
const mod = await WebAssembly.compile(await wasm());
wasmCache.set(path, mod);
return mod;
}
if (path === "arcjet_analyze_bindings_rate_limit.component.core2.wasm") {
const { wasm } = await import(
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core2.js"
);
const mod = await WebAssembly.compile(await wasm());
wasmCache.set(path, mod);
return mod;
}
if (path === "arcjet_analyze_bindings_rate_limit.component.core3.wasm") {
const { wasm } = await import(
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core3.js"
);
const mod = await WebAssembly.compile(await wasm());
wasmCache.set(path, mod);
return mod;
}
}

throw new Error(`Unknown path: ${path}`);
}

async function initRateLimit(): Promise<RateLimitAPI | undefined> {
try {
return rateLimit.instantiate(moduleFromPath, {
"arcjet:rate-limit/storage": {
get(key) {
return rateLimitCache.get(key);
},
set(key, value, ttl) {
console.log(ttl);
rateLimitCache.set(key, value, ttl);
},
},
"arcjet:rate-limit/time": {
now() {
return nowInSeconds();
},
},
});
} catch {
// TODO: Log unsupported wasm error
}
}

export async function fixedWindow(
config: {
key: string;
characteristics?: string[];
max: number;
window: number;
},
request: unknown,
): Promise<
| {
allowed: boolean;
max: number;
remaining: number;
reset: number;
}
| undefined
> {
const rl = await initRateLimit();

if (typeof rl !== "undefined") {
const configJson = JSON.stringify(config);
const requestJson = JSON.stringify(request);
const resultJson = rl.fixedWindow(configJson, requestJson);
// TODO: Try/catch and Validate
return JSON.parse(resultJson);
}
}
40 changes: 26 additions & 14 deletions analyze/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,35 @@ import { createConfig } from "@arcjet/rollup-config";
export default createConfig(import.meta.url, {
plugins: [
{
name: "externalize-wasm",
resolveId(source) {
name: "externalize-wasm-js",
resolveId(id) {
// We need to externalize this because otherwise some JS that edge can't
// understand gets included in the bundle
if (source === "./wasm/arcjet_analyze_js_req.js") {
return {
id: "./wasm/arcjet_analyze_js_req.js",
external: true,
};
if (id === "./wasm/arcjet_analyze_js_req.js") {
return { id, external: true };
}
// TODO: Generation of this file can be handled via rollup plugin so we
// wouldn't need to externalize here
if (source === "./wasm/arcjet.wasm.js") {
return {
id: "./wasm/arcjet.wasm.js",
external: true,
};
// TODO: Generation of the below files could be handled via rollup
// plugin so we wouldn't need to externalize here
if (id === "./wasm/arcjet.wasm.js") {
return { id, external: true };
}
if (
id ===
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core.js"
) {
return { id, external: true };
}
if (
id ===
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core2.js"
) {
return { id, external: true };
}
if (
id ===
"./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core3.js"
) {
return { id, external: true };
}
return null;
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @generated by wasm2module - DO NOT EDIT
/* tslint:disable */
/* eslint-disable */

/**
* This file contains an Arcjet Wasm binary inlined as a base64
* [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs)
* with the application/wasm MIME type.
*
* This was chosen to save on storage space over inlining the file directly as
* a Uint8Array, which would take up ~3x the space of the Wasm file. See
* https://blobfolio.com/2019/better-binary-batter-mixing-base64-and-uint8array/
* for more details.
*
* It is then decoded into an ArrayBuffer to be used directly via WebAssembly's
* `compile()` function in our entry point file.
*
* This is all done to avoid trying to read or bundle the Wasm asset in various
* ways based on the platform or bundler a user is targeting. One example being
* that Next.js requires special `asyncWebAssembly` webpack config to load our
* Wasm file if we don't do this.
*
* In the future, we hope to do away with this workaround when all bundlers
* properly support consistent asset bundling techniques.
*/

/**
* Returns an ArrayBuffer for the Arcjet Wasm binary, decoded from a base64 Data
* URL.
*/
export function wasm(): Promise<ArrayBuffer>;

Large diffs are not rendered by default.

Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @generated by wasm2module - DO NOT EDIT
/* tslint:disable */
/* eslint-disable */

/**
* This file contains an Arcjet Wasm binary inlined as a base64
* [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs)
* with the application/wasm MIME type.
*
* This was chosen to save on storage space over inlining the file directly as
* a Uint8Array, which would take up ~3x the space of the Wasm file. See
* https://blobfolio.com/2019/better-binary-batter-mixing-base64-and-uint8array/
* for more details.
*
* It is then decoded into an ArrayBuffer to be used directly via WebAssembly's
* `compile()` function in our entry point file.
*
* This is all done to avoid trying to read or bundle the Wasm asset in various
* ways based on the platform or bundler a user is targeting. One example being
* that Next.js requires special `asyncWebAssembly` webpack config to load our
* Wasm file if we don't do this.
*
* In the future, we hope to do away with this workaround when all bundlers
* properly support consistent asset bundling techniques.
*/

/**
* Returns an ArrayBuffer for the Arcjet Wasm binary, decoded from a base64 Data
* URL.
*/
export function wasm(): Promise<ArrayBuffer>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// @generated by wasm2module - DO NOT EDIT
/* eslint-disable */
// @ts-nocheck

/**
* This file contains an Arcjet Wasm binary inlined as a base64
* [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs)
* with the application/wasm MIME type.
*
* This was chosen to save on storage space over inlining the file directly as
* a Uint8Array, which would take up ~3x the space of the Wasm file. See
* https://blobfolio.com/2019/better-binary-batter-mixing-base64-and-uint8array/
* for more details.
*
* It is then decoded into an ArrayBuffer to be used directly via WebAssembly's
* `compile()` function in our entry point file.
*
* This is all done to avoid trying to read or bundle the Wasm asset in various
* ways based on the platform or bundler a user is targeting. One example being
* that Next.js requires special `asyncWebAssembly` webpack config to load our
* Wasm file if we don't do this.
*
* In the future, we hope to do away with this workaround when all bundlers
* properly support consistent asset bundling techniques.
*/

const wasmBase64 = "data:application/wasm;base64,AGFzbQEAAAABEAJgA39/fwBgBn9/f39/fwADAwIAAQQFAXABAgIHFAMBMAAAATEAAQgkaW1wb3J0cwEACiMCDQAgACABIAJBABEAAAsTACAAIAEgAiADIAQgBUEBEQEACwAuCXByb2R1Y2VycwEMcHJvY2Vzc2VkLWJ5AQ13aXQtY29tcG9uZW50BjAuMjAuMABtBG5hbWUAExJ3aXQtY29tcG9uZW50OnNoaW0BUQIAJmluZGlyZWN0LWFyY2pldDpyYXRlLWxpbWl0L3N0b3JhZ2UtZ2V0ASZpbmRpcmVjdC1hcmNqZXQ6cmF0ZS1saW1pdC9zdG9yYWdlLXNldA==";
/**
* Returns an ArrayBuffer for the Arcjet Wasm binary, decoded from a base64 Data
* URL.
*/
// TODO: Switch back to top-level await when our platforms all support it
export async function wasm() {
// This uses fetch to decode the wasm data url
const wasmDecode = await fetch(wasmBase64);
// And then we return it as an ArrayBuffer
return wasmDecode.arrayBuffer();
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @generated by wasm2module - DO NOT EDIT
/* tslint:disable */
/* eslint-disable */

/**
* This file contains an Arcjet Wasm binary inlined as a base64
* [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs)
* with the application/wasm MIME type.
*
* This was chosen to save on storage space over inlining the file directly as
* a Uint8Array, which would take up ~3x the space of the Wasm file. See
* https://blobfolio.com/2019/better-binary-batter-mixing-base64-and-uint8array/
* for more details.
*
* It is then decoded into an ArrayBuffer to be used directly via WebAssembly's
* `compile()` function in our entry point file.
*
* This is all done to avoid trying to read or bundle the Wasm asset in various
* ways based on the platform or bundler a user is targeting. One example being
* that Next.js requires special `asyncWebAssembly` webpack config to load our
* Wasm file if we don't do this.
*
* In the future, we hope to do away with this workaround when all bundlers
* properly support consistent asset bundling techniques.
*/

/**
* Returns an ArrayBuffer for the Arcjet Wasm binary, decoded from a base64 Data
* URL.
*/
export function wasm(): Promise<ArrayBuffer>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// @generated by wasm2module - DO NOT EDIT
/* eslint-disable */
// @ts-nocheck

/**
* This file contains an Arcjet Wasm binary inlined as a base64
* [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs)
* with the application/wasm MIME type.
*
* This was chosen to save on storage space over inlining the file directly as
* a Uint8Array, which would take up ~3x the space of the Wasm file. See
* https://blobfolio.com/2019/better-binary-batter-mixing-base64-and-uint8array/
* for more details.
*
* It is then decoded into an ArrayBuffer to be used directly via WebAssembly's
* `compile()` function in our entry point file.
*
* This is all done to avoid trying to read or bundle the Wasm asset in various
* ways based on the platform or bundler a user is targeting. One example being
* that Next.js requires special `asyncWebAssembly` webpack config to load our
* Wasm file if we don't do this.
*
* In the future, we hope to do away with this workaround when all bundlers
* properly support consistent asset bundling techniques.
*/

const wasmBase64 = "data:application/wasm;base64,AGFzbQEAAAABEAJgA39/fwBgBn9/f39/fwACGgMAATAAAAABMQABAAgkaW1wb3J0cwFwAQICCQgBAEEACwIAAQAuCXByb2R1Y2VycwEMcHJvY2Vzc2VkLWJ5AQ13aXQtY29tcG9uZW50BjAuMjAuMAAcBG5hbWUAFRR3aXQtY29tcG9uZW50OmZpeHVwcw==";
/**
* Returns an ArrayBuffer for the Arcjet Wasm binary, decoded from a base64 Data
* URL.
*/
// TODO: Switch back to top-level await when our platforms all support it
export async function wasm() {
// This uses fetch to decode the wasm data url
const wasmDecode = await fetch(wasmBase64);
// And then we return it as an ArrayBuffer
return wasmDecode.arrayBuffer();
}
Binary file not shown.
Loading

0 comments on commit adaaa9b

Please sign in to comment.