Skip to content

Commit

Permalink
feat: Support duration strings or integers on rate limit configuration (
Browse files Browse the repository at this point in the history
#192)

Closes #190 

This adds a new `@arcjet/duration` package that reimplements a subset of the Golang `ParseDuration` function. This allows us to support duration shorthand strings or integers (representing seconds) in our SDK while only working with the protobuf uint32 type on the wire.
  • Loading branch information
blaine-arcjet authored Feb 7, 2024
1 parent 6701b02 commit b173d83
Show file tree
Hide file tree
Showing 23 changed files with 899 additions and 21 deletions.
1 change: 1 addition & 0 deletions .github/.release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"analyze": "1.0.0-alpha.7",
"arcjet": "1.0.0-alpha.7",
"arcjet-next": "1.0.0-alpha.7",
"duration": "1.0.0-alpha.7",
"eslint-config": "1.0.0-alpha.7",
"ip": "1.0.0-alpha.7",
"logger": "1.0.0-alpha.7",
Expand Down
5 changes: 5 additions & 0 deletions .github/release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
"component": "@arcjet/next",
"skip-github-release": true
},
"duration": {
"component": "@arcjet/duration",
"skip-github-release": true
},
"eslint-config": {
"component": "@arcjet/eslint-config",
"skip-github-release": true
Expand Down Expand Up @@ -79,6 +83,7 @@
"@arcjet/analyze",
"arcjet",
"@arjcet/next",
"@arjcet/duration",
"@arcjet/eslint-config",
"@arcjet/ip",
"@arcjet/logger",
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ find a specific one through the categories and descriptions below.
into the Arcjet protocol.
- [`@arcjet/logger`](./logger/README.md): Logging interface which mirrors the
console interface but allows log levels.
- [`@arcjet/duration`](./duration/README.md): Utilities for parsing duration
strings into seconds integers.

### Internal development

Expand Down
13 changes: 7 additions & 6 deletions arcjet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
Timestamp,
} from "@arcjet/protocol/proto.js";
import * as analyze from "@arcjet/analyze";
import * as duration from "@arcjet/duration";
import { Logger } from "@arcjet/logger";

export * from "@arcjet/protocol";
Expand Down Expand Up @@ -421,23 +422,23 @@ type TokenBucketRateLimitOptions = {
match?: string;
characteristics?: string[];
refillRate: number;
interval: number;
interval: string | number;
capacity: number;
};

type FixedWindowRateLimitOptions = {
mode?: ArcjetMode;
match?: string;
characteristics?: string[];
window: string;
window: string | number;
max: number;
};

type SlidingWindowRateLimitOptions = {
mode?: ArcjetMode;
match?: string;
characteristics?: string[];
interval: number;
interval: string | number;
max: number;
};

Expand Down Expand Up @@ -605,7 +606,7 @@ export function tokenBucket(
const characteristics = opt.characteristics;

const refillRate = opt.refillRate;
const interval = opt.interval;
const interval = duration.parse(opt.interval);
const capacity = opt.capacity;

rules.push({
Expand Down Expand Up @@ -640,7 +641,7 @@ export function fixedWindow(
const characteristics = opt.characteristics;

const max = opt.max;
const window = opt.window;
const window = duration.parse(opt.window);

rules.push({
type: "RATE_LIMIT",
Expand Down Expand Up @@ -684,7 +685,7 @@ export function slidingWindow(
const characteristics = opt.characteristics;

const max = opt.max;
const interval = opt.interval;
const interval = duration.parse(opt.interval);

rules.push({
type: "RATE_LIMIT",
Expand Down
1 change: 1 addition & 0 deletions arcjet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
},
"dependencies": {
"@arcjet/analyze": "1.0.0-alpha.7",
"@arcjet/duration": "1.0.0-alpha.7",
"@arcjet/logger": "1.0.0-alpha.7",
"@arcjet/protocol": "1.0.0-alpha.7"
},
Expand Down
102 changes: 92 additions & 10 deletions arcjet/test/index.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1948,6 +1948,36 @@ describe("Primitive > tokenBucket", () => {
expect(rule).toHaveProperty("mode", "LIVE");
});

test("can specify interval as a string duration", async () => {
const options = {
refillRate: 60,
interval: "60s",
capacity: 120,
};

const rules = tokenBucket(options);
expect(rules).toHaveLength(1);
expect(rules[0].type).toEqual("RATE_LIMIT");
expect(rules[0]).toHaveProperty("refillRate", 60);
expect(rules[0]).toHaveProperty("interval", 60);
expect(rules[0]).toHaveProperty("capacity", 120);
});

test("can specify interval as an integer duration", async () => {
const options = {
refillRate: 60,
interval: 60,
capacity: 120,
};

const rules = tokenBucket(options);
expect(rules).toHaveLength(1);
expect(rules[0].type).toEqual("RATE_LIMIT");
expect(rules[0]).toHaveProperty("refillRate", 60);
expect(rules[0]).toHaveProperty("interval", 60);
expect(rules[0]).toHaveProperty("capacity", 120);
});

test("produces a rules based on single `limit` specified", async () => {
const options = {
match: "/test",
Expand Down Expand Up @@ -2096,6 +2126,32 @@ describe("Primitive > fixedWindow", () => {
expect(rule).toHaveProperty("mode", "LIVE");
});

test("can specify window as a string duration", async () => {
const options = {
window: "60s",
max: 1,
};

const rules = fixedWindow(options);
expect(rules).toHaveLength(1);
expect(rules[0].type).toEqual("RATE_LIMIT");
expect(rules[0]).toHaveProperty("window", 60);
expect(rules[0]).toHaveProperty("max", 1);
});

test("can specify window as an integer duration", async () => {
const options = {
window: 60,
max: 1,
};

const rules = fixedWindow(options);
expect(rules).toHaveLength(1);
expect(rules[0].type).toEqual("RATE_LIMIT");
expect(rules[0]).toHaveProperty("window", 60);
expect(rules[0]).toHaveProperty("max", 1);
});

test("produces a rules based on single `limit` specified", async () => {
const options = {
match: "/test",
Expand All @@ -2111,7 +2167,7 @@ describe("Primitive > fixedWindow", () => {
expect(rules[0]).toHaveProperty("match", "/test");
expect(rules[0]).toHaveProperty("characteristics", ["ip.src"]);
expect(rules[0]).toHaveProperty("algorithm", "FIXED_WINDOW");
expect(rules[0]).toHaveProperty("window", "1h");
expect(rules[0]).toHaveProperty("window", 3600);
expect(rules[0]).toHaveProperty("max", 1);
});

Expand Down Expand Up @@ -2140,7 +2196,7 @@ describe("Primitive > fixedWindow", () => {
match: "/test",
characteristics: ["ip.src"],
algorithm: "FIXED_WINDOW",
window: "1h",
window: 3600,
max: 1,
}),
expect.objectContaining({
Expand All @@ -2149,7 +2205,7 @@ describe("Primitive > fixedWindow", () => {
match: "/test-double",
characteristics: ["ip.src"],
algorithm: "FIXED_WINDOW",
window: "2h",
window: 7200,
max: 2,
}),
]);
Expand Down Expand Up @@ -2187,7 +2243,7 @@ describe("Primitive > fixedWindow", () => {
match: undefined,
characteristics: undefined,
algorithm: "FIXED_WINDOW",
window: "1h",
window: 3600,
max: 1,
}),
expect.objectContaining({
Expand All @@ -2196,7 +2252,7 @@ describe("Primitive > fixedWindow", () => {
match: undefined,
characteristics: undefined,
algorithm: "FIXED_WINDOW",
window: "2h",
window: 7200,
max: 2,
}),
]);
Expand Down Expand Up @@ -2234,6 +2290,32 @@ describe("Primitive > slidingWindow", () => {
expect(rule).toHaveProperty("mode", "LIVE");
});

test("can specify interval as a string duration", async () => {
const options = {
interval: "60s",
max: 1,
};

const rules = slidingWindow(options);
expect(rules).toHaveLength(1);
expect(rules[0].type).toEqual("RATE_LIMIT");
expect(rules[0]).toHaveProperty("interval", 60);
expect(rules[0]).toHaveProperty("max", 1);
});

test("can specify interval as an integer duration", async () => {
const options = {
interval: 60,
max: 1,
};

const rules = slidingWindow(options);
expect(rules).toHaveLength(1);
expect(rules[0].type).toEqual("RATE_LIMIT");
expect(rules[0]).toHaveProperty("interval", 60);
expect(rules[0]).toHaveProperty("max", 1);
});

test("produces a rules based on single `limit` specified", async () => {
const options = {
match: "/test",
Expand Down Expand Up @@ -2390,7 +2472,7 @@ describe("Primitive > rateLimit", () => {
expect(rules[0]).toHaveProperty("match", "/test");
expect(rules[0]).toHaveProperty("characteristics", ["ip.src"]);
expect(rules[0]).toHaveProperty("algorithm", "FIXED_WINDOW");
expect(rules[0]).toHaveProperty("window", "1h");
expect(rules[0]).toHaveProperty("window", 3600);
expect(rules[0]).toHaveProperty("max", 1);
});

Expand Down Expand Up @@ -2419,7 +2501,7 @@ describe("Primitive > rateLimit", () => {
match: "/test",
characteristics: ["ip.src"],
algorithm: "FIXED_WINDOW",
window: "1h",
window: 3600,
max: 1,
}),
expect.objectContaining({
Expand All @@ -2428,7 +2510,7 @@ describe("Primitive > rateLimit", () => {
match: "/test-double",
characteristics: ["ip.src"],
algorithm: "FIXED_WINDOW",
window: "2h",
window: 7200,
max: 2,
}),
]);
Expand Down Expand Up @@ -2466,7 +2548,7 @@ describe("Primitive > rateLimit", () => {
match: undefined,
characteristics: undefined,
algorithm: "FIXED_WINDOW",
window: "1h",
window: 3600,
max: 1,
}),
expect.objectContaining({
Expand All @@ -2475,7 +2557,7 @@ describe("Primitive > rateLimit", () => {
match: undefined,
characteristics: undefined,
algorithm: "FIXED_WINDOW",
window: "2h",
window: 7200,
max: 2,
}),
]);
Expand Down
6 changes: 6 additions & 0 deletions duration/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/.turbo/
/coverage/
/node_modules/
*.d.ts
*.js
!*.config.js
4 changes: 4 additions & 0 deletions duration/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["@arcjet/eslint-config"],
};
Loading

0 comments on commit b173d83

Please sign in to comment.