@valentinkolb/sync 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +72 -0
- package/CLAUDE.md +106 -0
- package/LICENSE +21 -0
- package/README.md +292 -0
- package/bun.lock +29 -0
- package/compose.test.yml +7 -0
- package/index.ts +18 -0
- package/package.json +21 -0
- package/src/jobs.ts +568 -0
- package/src/mutex.ts +203 -0
- package/src/ratelimit.ts +143 -0
- package/tests/jobs.test.ts +465 -0
- package/tests/mutex.test.ts +223 -0
- package/tests/preload.ts +2 -0
- package/tests/ratelimit.test.ts +119 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { test, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { redis } from "bun";
|
|
3
|
+
import { ratelimit, RateLimitError } from "../index";
|
|
4
|
+
|
|
5
|
+
// Clean up Redis before each test
|
|
6
|
+
beforeEach(async () => {
|
|
7
|
+
const keys = await redis.send("KEYS", ["ratelimit:test:*"]);
|
|
8
|
+
if (Array.isArray(keys) && keys.length > 0) {
|
|
9
|
+
await redis.send("DEL", keys as string[]);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("allows requests within limit", async () => {
|
|
14
|
+
const limiter = ratelimit.create({
|
|
15
|
+
limit: 5,
|
|
16
|
+
windowSecs: 60,
|
|
17
|
+
prefix: "ratelimit:test",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < 5; i++) {
|
|
21
|
+
const result = await limiter.check("user:1");
|
|
22
|
+
expect(result.limited).toBe(false);
|
|
23
|
+
expect(result.remaining).toBe(5 - i - 1);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("blocks requests over limit", async () => {
|
|
28
|
+
const limiter = ratelimit.create({
|
|
29
|
+
limit: 3,
|
|
30
|
+
windowSecs: 60,
|
|
31
|
+
prefix: "ratelimit:test",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Use up the limit
|
|
35
|
+
for (let i = 0; i < 3; i++) {
|
|
36
|
+
await limiter.check("user:2");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Next request should be limited
|
|
40
|
+
const result = await limiter.check("user:2");
|
|
41
|
+
expect(result.limited).toBe(true);
|
|
42
|
+
expect(result.remaining).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("checkOrThrow throws RateLimitError when limited", async () => {
|
|
46
|
+
const limiter = ratelimit.create({
|
|
47
|
+
limit: 1,
|
|
48
|
+
windowSecs: 60,
|
|
49
|
+
prefix: "ratelimit:test",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Use up the limit
|
|
53
|
+
await limiter.check("user:3");
|
|
54
|
+
|
|
55
|
+
// Should throw
|
|
56
|
+
try {
|
|
57
|
+
await limiter.checkOrThrow("user:3");
|
|
58
|
+
expect(true).toBe(false); // Should not reach here
|
|
59
|
+
} catch (e) {
|
|
60
|
+
expect(e).toBeInstanceOf(RateLimitError);
|
|
61
|
+
expect((e as RateLimitError).remaining).toBe(0);
|
|
62
|
+
expect((e as RateLimitError).resetIn).toBeGreaterThan(0);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("different identifiers have separate limits", async () => {
|
|
67
|
+
const limiter = ratelimit.create({
|
|
68
|
+
limit: 2,
|
|
69
|
+
windowSecs: 60,
|
|
70
|
+
prefix: "ratelimit:test",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// User A uses their limit
|
|
74
|
+
await limiter.check("user:a");
|
|
75
|
+
await limiter.check("user:a");
|
|
76
|
+
const resultA = await limiter.check("user:a");
|
|
77
|
+
expect(resultA.limited).toBe(true);
|
|
78
|
+
|
|
79
|
+
// User B should still have their limit
|
|
80
|
+
const resultB = await limiter.check("user:b");
|
|
81
|
+
expect(resultB.limited).toBe(false);
|
|
82
|
+
expect(resultB.remaining).toBe(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("resetIn returns time until window reset", async () => {
|
|
86
|
+
const limiter = ratelimit.create({
|
|
87
|
+
limit: 10,
|
|
88
|
+
windowSecs: 60,
|
|
89
|
+
prefix: "ratelimit:test",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const result = await limiter.check("user:4");
|
|
93
|
+
expect(result.resetIn).toBeGreaterThan(0);
|
|
94
|
+
expect(result.resetIn).toBeLessThanOrEqual(60000);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("sliding window counts previous window", async () => {
|
|
98
|
+
const limiter = ratelimit.create({
|
|
99
|
+
limit: 10,
|
|
100
|
+
windowSecs: 1, // 1 second window for faster testing
|
|
101
|
+
prefix: "ratelimit:test",
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Make 8 requests
|
|
105
|
+
for (let i = 0; i < 8; i++) {
|
|
106
|
+
await limiter.check("user:5");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Wait for half the window
|
|
110
|
+
await Bun.sleep(500);
|
|
111
|
+
|
|
112
|
+
// Previous window count is weighted, so we should still have limited capacity
|
|
113
|
+
// 8 requests * 0.5 weight = 4 weighted from previous
|
|
114
|
+
// So we should have ~6 remaining
|
|
115
|
+
const result = await limiter.check("user:5");
|
|
116
|
+
expect(result.limited).toBe(false);
|
|
117
|
+
expect(result.remaining).toBeLessThan(10);
|
|
118
|
+
expect(result.remaining).toBeGreaterThan(0);
|
|
119
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false,
|
|
28
|
+
},
|
|
29
|
+
"include": ["index.ts", "src/**/*.ts"],
|
|
30
|
+
"exclude": ["node_modules", "old_impl"],
|
|
31
|
+
}
|