@thirdweb-dev/service-utils 0.9.1 → 0.9.2-nightly-4c1f384a635054a8cfaaa487e3ba7ada63b221ec-20250415110008
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/dist/cjs/core/rateLimit/index.js +35 -33
- package/dist/cjs/core/rateLimit/index.js.map +1 -1
- package/dist/esm/core/rateLimit/index.js +35 -33
- package/dist/esm/core/rateLimit/index.js.map +1 -1
- package/dist/types/core/rateLimit/index.d.ts +8 -8
- package/dist/types/core/rateLimit/index.d.ts.map +1 -1
- package/package.json +1 -1
@@ -1,17 +1,16 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
3
|
exports.rateLimit = rateLimit;
|
4
|
-
const
|
4
|
+
const SLIDING_WINDOW_SECONDS = 10;
|
5
|
+
/**
|
6
|
+
* Increments the request count for this team and returns whether the team has hit their rate limit.
|
7
|
+
* Uses a sliding 10 second window.
|
8
|
+
* @param args
|
9
|
+
* @returns
|
10
|
+
*/
|
5
11
|
async function rateLimit(args) {
|
6
|
-
const { team, limitPerSecond, serviceConfig, redis,
|
7
|
-
const
|
8
|
-
if (!shouldSampleRequest) {
|
9
|
-
return {
|
10
|
-
rateLimited: false,
|
11
|
-
requestCount: 0,
|
12
|
-
rateLimit: 0,
|
13
|
-
};
|
14
|
-
}
|
12
|
+
const { team, limitPerSecond, serviceConfig, redis, increment = 1 } = args;
|
13
|
+
const { serviceScope } = serviceConfig;
|
15
14
|
if (limitPerSecond === 0) {
|
16
15
|
// No rate limit is provided. Assume the request is not rate limited.
|
17
16
|
return {
|
@@ -20,40 +19,43 @@ async function rateLimit(args) {
|
|
20
19
|
rateLimit: 0,
|
21
20
|
};
|
22
21
|
}
|
23
|
-
|
24
|
-
|
25
|
-
const
|
26
|
-
|
27
|
-
const
|
28
|
-
|
29
|
-
|
30
|
-
// Get the limit for this window accounting for the sample rate.
|
31
|
-
const limitPerWindow = limitPerSecond * sampleRate * RATE_LIMIT_WINDOW_SECONDS;
|
32
|
-
if (requestCount > limitPerWindow) {
|
22
|
+
// Enforce rate limit: sum the total requests in the last `SLIDING_WINDOW_SECONDS` seconds.
|
23
|
+
const currentSecond = Math.floor(Date.now() / 1000);
|
24
|
+
const keys = Array.from({ length: SLIDING_WINDOW_SECONDS }, (_, i) => getRequestCountAtSecondCacheKey(serviceScope, team.id, currentSecond - i));
|
25
|
+
const counts = await redis.mget(keys);
|
26
|
+
const totalCount = counts.reduce((sum, count) => sum + (count ? Number.parseInt(count) : 0), 0);
|
27
|
+
const limitPerWindow = limitPerSecond * SLIDING_WINDOW_SECONDS;
|
28
|
+
if (totalCount > limitPerWindow) {
|
33
29
|
return {
|
34
30
|
rateLimited: true,
|
35
|
-
requestCount,
|
31
|
+
requestCount: totalCount,
|
36
32
|
rateLimit: limitPerWindow,
|
37
33
|
status: 429,
|
38
|
-
errorMessage: `You've exceeded your ${serviceScope} rate limit at ${limitPerSecond} reqs/sec.
|
34
|
+
errorMessage: `You've exceeded your ${serviceScope} rate limit at ${limitPerSecond} reqs/sec. Please upgrade your plan to get higher rate limits.`,
|
39
35
|
errorCode: "RATE_LIMIT_EXCEEDED",
|
40
36
|
};
|
41
37
|
}
|
42
|
-
//
|
43
|
-
(async () =>
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
38
|
+
// Non-blocking: increment the request count for the current second.
|
39
|
+
(async () => {
|
40
|
+
try {
|
41
|
+
const key = getRequestCountAtSecondCacheKey(serviceScope, team.id, currentSecond);
|
42
|
+
await redis.incrby(key, increment);
|
43
|
+
// If this is the first time setting this key, expire it after the sliding window is past.
|
44
|
+
if (counts[0] === null) {
|
45
|
+
await redis.expire(key, SLIDING_WINDOW_SECONDS + 1);
|
46
|
+
}
|
49
47
|
}
|
50
|
-
|
51
|
-
|
52
|
-
|
48
|
+
catch (error) {
|
49
|
+
console.error("Error updating rate limit key:", error);
|
50
|
+
}
|
51
|
+
})();
|
53
52
|
return {
|
54
53
|
rateLimited: false,
|
55
|
-
requestCount:
|
54
|
+
requestCount: totalCount + increment,
|
56
55
|
rateLimit: limitPerWindow,
|
57
56
|
};
|
58
57
|
}
|
58
|
+
function getRequestCountAtSecondCacheKey(serviceScope, teamId, second) {
|
59
|
+
return `rate-limit:${serviceScope}:${teamId}:${second}`;
|
60
|
+
}
|
59
61
|
//# sourceMappingURL=index.js.map
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/core/rateLimit/index.ts"],"names":[],"mappings":";;
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/core/rateLimit/index.ts"],"names":[],"mappings":";;AAkBA,8BAsEC;AArFD,MAAM,sBAAsB,GAAG,EAAE,CAAC;AASlC;;;;;GAKG;AACI,KAAK,UAAU,SAAS,CAAC,IAU/B;IACC,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC;IAC3E,MAAM,EAAE,YAAY,EAAE,GAAG,aAAa,CAAC;IAEvC,IAAI,cAAc,KAAK,CAAC,EAAE,CAAC;QACzB,qEAAqE;QACrE,OAAO;YACL,WAAW,EAAE,KAAK;YAClB,YAAY,EAAE,CAAC;YACf,SAAS,EAAE,CAAC;SACb,CAAC;IACJ,CAAC;IAED,2FAA2F;IAC3F,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,sBAAsB,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACnE,+BAA+B,CAAC,YAAY,EAAE,IAAI,CAAC,EAAE,EAAE,aAAa,GAAG,CAAC,CAAC,CAC1E,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAC9B,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAC1D,CAAC,CACF,CAAC;IAEF,MAAM,cAAc,GAAG,cAAc,GAAG,sBAAsB,CAAC;IAE/D,IAAI,UAAU,GAAG,cAAc,EAAE,CAAC;QAChC,OAAO;YACL,WAAW,EAAE,IAAI;YACjB,YAAY,EAAE,UAAU;YACxB,SAAS,EAAE,cAAc;YACzB,MAAM,EAAE,GAAG;YACX,YAAY,EAAE,wBAAwB,YAAY,kBAAkB,cAAc,gEAAgE;YAClJ,SAAS,EAAE,qBAAqB;SACjC,CAAC;IACJ,CAAC;IAED,oEAAoE;IACpE,CAAC,KAAK,IAAI,EAAE;QACV,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,+BAA+B,CACzC,YAAY,EACZ,IAAI,CAAC,EAAE,EACP,aAAa,CACd,CAAC;YACF,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YACnC,0FAA0F;YAC1F,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;gBACvB,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,sBAAsB,GAAG,CAAC,CAAC,CAAC;YACtD,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,gCAAgC,EAAE,KAAK,CAAC,CAAC;QACzD,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;IAEL,OAAO;QACL,WAAW,EAAE,KAAK;QAClB,YAAY,EAAE,UAAU,GAAG,SAAS;QACpC,SAAS,EAAE,cAAc;KAC1B,CAAC;AACJ,CAAC;AAED,SAAS,+BAA+B,CACtC,YAA+C,EAC/C,MAAc,EACd,MAAc;IAEd,OAAO,cAAc,YAAY,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;AAC1D,CAAC"}
|
@@ -1,14 +1,13 @@
|
|
1
|
-
const
|
1
|
+
const SLIDING_WINDOW_SECONDS = 10;
|
2
|
+
/**
|
3
|
+
* Increments the request count for this team and returns whether the team has hit their rate limit.
|
4
|
+
* Uses a sliding 10 second window.
|
5
|
+
* @param args
|
6
|
+
* @returns
|
7
|
+
*/
|
2
8
|
export async function rateLimit(args) {
|
3
|
-
const { team, limitPerSecond, serviceConfig, redis,
|
4
|
-
const
|
5
|
-
if (!shouldSampleRequest) {
|
6
|
-
return {
|
7
|
-
rateLimited: false,
|
8
|
-
requestCount: 0,
|
9
|
-
rateLimit: 0,
|
10
|
-
};
|
11
|
-
}
|
9
|
+
const { team, limitPerSecond, serviceConfig, redis, increment = 1 } = args;
|
10
|
+
const { serviceScope } = serviceConfig;
|
12
11
|
if (limitPerSecond === 0) {
|
13
12
|
// No rate limit is provided. Assume the request is not rate limited.
|
14
13
|
return {
|
@@ -17,40 +16,43 @@ export async function rateLimit(args) {
|
|
17
16
|
rateLimit: 0,
|
18
17
|
};
|
19
18
|
}
|
20
|
-
|
21
|
-
|
22
|
-
const
|
23
|
-
|
24
|
-
const
|
25
|
-
|
26
|
-
|
27
|
-
// Get the limit for this window accounting for the sample rate.
|
28
|
-
const limitPerWindow = limitPerSecond * sampleRate * RATE_LIMIT_WINDOW_SECONDS;
|
29
|
-
if (requestCount > limitPerWindow) {
|
19
|
+
// Enforce rate limit: sum the total requests in the last `SLIDING_WINDOW_SECONDS` seconds.
|
20
|
+
const currentSecond = Math.floor(Date.now() / 1000);
|
21
|
+
const keys = Array.from({ length: SLIDING_WINDOW_SECONDS }, (_, i) => getRequestCountAtSecondCacheKey(serviceScope, team.id, currentSecond - i));
|
22
|
+
const counts = await redis.mget(keys);
|
23
|
+
const totalCount = counts.reduce((sum, count) => sum + (count ? Number.parseInt(count) : 0), 0);
|
24
|
+
const limitPerWindow = limitPerSecond * SLIDING_WINDOW_SECONDS;
|
25
|
+
if (totalCount > limitPerWindow) {
|
30
26
|
return {
|
31
27
|
rateLimited: true,
|
32
|
-
requestCount,
|
28
|
+
requestCount: totalCount,
|
33
29
|
rateLimit: limitPerWindow,
|
34
30
|
status: 429,
|
35
|
-
errorMessage: `You've exceeded your ${serviceScope} rate limit at ${limitPerSecond} reqs/sec.
|
31
|
+
errorMessage: `You've exceeded your ${serviceScope} rate limit at ${limitPerSecond} reqs/sec. Please upgrade your plan to get higher rate limits.`,
|
36
32
|
errorCode: "RATE_LIMIT_EXCEEDED",
|
37
33
|
};
|
38
34
|
}
|
39
|
-
//
|
40
|
-
(async () =>
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
35
|
+
// Non-blocking: increment the request count for the current second.
|
36
|
+
(async () => {
|
37
|
+
try {
|
38
|
+
const key = getRequestCountAtSecondCacheKey(serviceScope, team.id, currentSecond);
|
39
|
+
await redis.incrby(key, increment);
|
40
|
+
// If this is the first time setting this key, expire it after the sliding window is past.
|
41
|
+
if (counts[0] === null) {
|
42
|
+
await redis.expire(key, SLIDING_WINDOW_SECONDS + 1);
|
43
|
+
}
|
46
44
|
}
|
47
|
-
|
48
|
-
|
49
|
-
|
45
|
+
catch (error) {
|
46
|
+
console.error("Error updating rate limit key:", error);
|
47
|
+
}
|
48
|
+
})();
|
50
49
|
return {
|
51
50
|
rateLimited: false,
|
52
|
-
requestCount:
|
51
|
+
requestCount: totalCount + increment,
|
53
52
|
rateLimit: limitPerWindow,
|
54
53
|
};
|
55
54
|
}
|
55
|
+
function getRequestCountAtSecondCacheKey(serviceScope, teamId, second) {
|
56
|
+
return `rate-limit:${serviceScope}:${teamId}:${second}`;
|
57
|
+
}
|
56
58
|
//# sourceMappingURL=index.js.map
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/core/rateLimit/index.ts"],"names":[],"mappings":"AAGA,MAAM,
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/core/rateLimit/index.ts"],"names":[],"mappings":"AAGA,MAAM,sBAAsB,GAAG,EAAE,CAAC;AASlC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAU/B;IACC,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC;IAC3E,MAAM,EAAE,YAAY,EAAE,GAAG,aAAa,CAAC;IAEvC,IAAI,cAAc,KAAK,CAAC,EAAE,CAAC;QACzB,qEAAqE;QACrE,OAAO;YACL,WAAW,EAAE,KAAK;YAClB,YAAY,EAAE,CAAC;YACf,SAAS,EAAE,CAAC;SACb,CAAC;IACJ,CAAC;IAED,2FAA2F;IAC3F,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,sBAAsB,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACnE,+BAA+B,CAAC,YAAY,EAAE,IAAI,CAAC,EAAE,EAAE,aAAa,GAAG,CAAC,CAAC,CAC1E,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAC9B,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAC1D,CAAC,CACF,CAAC;IAEF,MAAM,cAAc,GAAG,cAAc,GAAG,sBAAsB,CAAC;IAE/D,IAAI,UAAU,GAAG,cAAc,EAAE,CAAC;QAChC,OAAO;YACL,WAAW,EAAE,IAAI;YACjB,YAAY,EAAE,UAAU;YACxB,SAAS,EAAE,cAAc;YACzB,MAAM,EAAE,GAAG;YACX,YAAY,EAAE,wBAAwB,YAAY,kBAAkB,cAAc,gEAAgE;YAClJ,SAAS,EAAE,qBAAqB;SACjC,CAAC;IACJ,CAAC;IAED,oEAAoE;IACpE,CAAC,KAAK,IAAI,EAAE;QACV,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,+BAA+B,CACzC,YAAY,EACZ,IAAI,CAAC,EAAE,EACP,aAAa,CACd,CAAC;YACF,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YACnC,0FAA0F;YAC1F,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;gBACvB,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,sBAAsB,GAAG,CAAC,CAAC,CAAC;YACtD,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,gCAAgC,EAAE,KAAK,CAAC,CAAC;QACzD,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;IAEL,OAAO;QACL,WAAW,EAAE,KAAK;QAClB,YAAY,EAAE,UAAU,GAAG,SAAS;QACpC,SAAS,EAAE,cAAc;KAC1B,CAAC;AACJ,CAAC;AAED,SAAS,+BAA+B,CACtC,YAA+C,EAC/C,MAAc,EACd,MAAc;IAEd,OAAO,cAAc,YAAY,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;AAC1D,CAAC"}
|
@@ -1,21 +1,21 @@
|
|
1
1
|
import type { CoreServiceConfig, TeamResponse } from "../api.js";
|
2
2
|
import type { RateLimitResult } from "./types.js";
|
3
3
|
type IRedis = {
|
4
|
-
get: (key: string) => Promise<string | null>;
|
5
|
-
expire(key: string, seconds: number): Promise<number>;
|
6
4
|
incrby(key: string, value: number): Promise<number>;
|
5
|
+
mget(keys: string[]): Promise<(string | null)[]>;
|
6
|
+
expire(key: string, seconds: number): Promise<number>;
|
7
7
|
};
|
8
|
+
/**
|
9
|
+
* Increments the request count for this team and returns whether the team has hit their rate limit.
|
10
|
+
* Uses a sliding 10 second window.
|
11
|
+
* @param args
|
12
|
+
* @returns
|
13
|
+
*/
|
8
14
|
export declare function rateLimit(args: {
|
9
15
|
team: TeamResponse;
|
10
16
|
limitPerSecond: number;
|
11
17
|
serviceConfig: CoreServiceConfig;
|
12
18
|
redis: IRedis;
|
13
|
-
/**
|
14
|
-
* Sample requests to reduce load on Redis.
|
15
|
-
* This scales down the request count and the rate limit threshold.
|
16
|
-
* @default 1.0
|
17
|
-
*/
|
18
|
-
sampleRate?: number;
|
19
19
|
/**
|
20
20
|
* The number of requests to increment by.
|
21
21
|
* @default 1
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/core/rateLimit/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAKlD,KAAK,MAAM,GAAG;IACZ,
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/core/rateLimit/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAKlD,KAAK,MAAM,GAAG;IACZ,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;IACjD,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvD,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,IAAI,EAAE,YAAY,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,iBAAiB,CAAC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,eAAe,CAAC,CA4D3B"}
|