@thi.ng/server 0.6.0 → 0.7.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/CHANGELOG.md +10 -1
- package/README.md +3 -2
- package/interceptors/rate-limit.d.ts +46 -42
- package/interceptors/rate-limit.js +15 -65
- package/package.json +16 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
- **Last updated**: 2025-
|
|
3
|
+
- **Last updated**: 2025-03-09T19:21:53Z
|
|
4
4
|
- **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
|
|
5
5
|
|
|
6
6
|
All notable changes to this project will be documented in this file.
|
|
@@ -11,6 +11,15 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
|
|
|
11
11
|
**Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
|
|
12
12
|
and/or version bumps of transitive dependencies.
|
|
13
13
|
|
|
14
|
+
## [0.7.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/server@0.7.0) (2025-03-09)
|
|
15
|
+
|
|
16
|
+
#### 🚀 Features
|
|
17
|
+
|
|
18
|
+
- update RateLimiter to use leaky buckets ([f342fde](https://github.com/thi-ng/umbrella/commit/f342fde))
|
|
19
|
+
- update `RateLimterOpts`
|
|
20
|
+
- update/simplify `RateLimiter` interceptor
|
|
21
|
+
- update deps
|
|
22
|
+
|
|
14
23
|
## [0.6.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/server@0.6.0) (2025-02-21)
|
|
15
24
|
|
|
16
25
|
#### 🚀 Features
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://mastodon.thi.ng/@toxi)
|
|
8
8
|
|
|
9
9
|
> [!NOTE]
|
|
10
|
-
> This is one of
|
|
10
|
+
> This is one of 203 standalone projects, maintained as part
|
|
11
11
|
> of the [@thi.ng/umbrella](https://github.com/thi-ng/umbrella/) monorepo
|
|
12
12
|
> and anti-framework.
|
|
13
13
|
>
|
|
@@ -149,7 +149,7 @@ For Node.js REPL:
|
|
|
149
149
|
const ser = await import("@thi.ng/server");
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
Package sizes (brotli'd, pre-treeshake): ESM: 5.
|
|
152
|
+
Package sizes (brotli'd, pre-treeshake): ESM: 5.15 KB
|
|
153
153
|
|
|
154
154
|
## Dependencies
|
|
155
155
|
|
|
@@ -159,6 +159,7 @@ Package sizes (brotli'd, pre-treeshake): ESM: 5.28 KB
|
|
|
159
159
|
- [@thi.ng/checks](https://github.com/thi-ng/umbrella/tree/develop/packages/checks)
|
|
160
160
|
- [@thi.ng/errors](https://github.com/thi-ng/umbrella/tree/develop/packages/errors)
|
|
161
161
|
- [@thi.ng/file-io](https://github.com/thi-ng/umbrella/tree/develop/packages/file-io)
|
|
162
|
+
- [@thi.ng/leaky-bucket](https://github.com/thi-ng/umbrella/tree/develop/packages/leaky-bucket)
|
|
162
163
|
- [@thi.ng/logger](https://github.com/thi-ng/umbrella/tree/develop/packages/logger)
|
|
163
164
|
- [@thi.ng/mime](https://github.com/thi-ng/umbrella/tree/develop/packages/mime)
|
|
164
165
|
- [@thi.ng/paths](https://github.com/thi-ng/umbrella/tree/develop/packages/paths)
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { Fn, Fn2, Maybe } from "@thi.ng/api";
|
|
2
|
-
import {
|
|
2
|
+
import { LeakyBucketMap } from "@thi.ng/leaky-bucket";
|
|
3
3
|
import type { Interceptor, RequestCtx } from "../api.js";
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for {@link RateLimiter}.
|
|
6
|
+
*
|
|
7
|
+
* References:
|
|
8
|
+
* - https://thi.ng/leaky-bucket
|
|
9
|
+
* - https://en.wikipedia.org/wiki/Leaky_bucket
|
|
10
|
+
*/
|
|
4
11
|
export interface RateLimitOpts<T extends RequestCtx = RequestCtx> {
|
|
5
12
|
/**
|
|
6
13
|
* Function to produce a unique ID for rate limiting a client. By default
|
|
@@ -8,23 +15,44 @@ export interface RateLimitOpts<T extends RequestCtx = RequestCtx> {
|
|
|
8
15
|
* client will NOT be rate limited.
|
|
9
16
|
*/
|
|
10
17
|
id?: Fn<T, Maybe<string>>;
|
|
18
|
+
/**
|
|
19
|
+
* Max. number of concurrently active client IDs/buckets. A bucket is
|
|
20
|
+
* created for each unique ID produced via {@link RateLimitOpts.id}.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* A bucket's counter is incremented for each related request, up to the
|
|
24
|
+
* configured {@link RateLimitOpts.bucketCapacity}. Once a bucket's capacity
|
|
25
|
+
* has been reached, any new requests are dropped by the interceptor. Bucket
|
|
26
|
+
* counters are slowly decremented every
|
|
27
|
+
* {@link RateLimiterOpts.leakInterval} milliseconds, thus slowly allowing
|
|
28
|
+
* again new requests. Once a bucket's counter goes back to zero, the bucket
|
|
29
|
+
* will be removed.
|
|
30
|
+
*
|
|
31
|
+
* @defaultValue 1000
|
|
32
|
+
*/
|
|
33
|
+
maxBuckets: number;
|
|
11
34
|
/**
|
|
12
35
|
* Function to compute the max number of allowed requests for given client
|
|
13
|
-
* ID
|
|
14
|
-
*
|
|
36
|
+
* ID bucket. If given as number, that limit will be uniformly enforced for
|
|
37
|
+
* all clients. If given as function, per-client quotas can be defined.
|
|
15
38
|
*
|
|
16
39
|
* @remarks
|
|
17
|
-
*
|
|
18
|
-
* when its
|
|
19
|
-
* {@link RateLimitOpts.
|
|
40
|
+
* Capacities are only computed whenever a new client ID is first
|
|
41
|
+
* encountered or when its existing bucket is empty again (also see
|
|
42
|
+
* {@link RateLimitOpts.leakInterval}).
|
|
43
|
+
*
|
|
44
|
+
* @defaultValue 60
|
|
20
45
|
*/
|
|
21
|
-
|
|
46
|
+
bucketCapacity: number | Fn2<T, string, number>;
|
|
22
47
|
/**
|
|
23
|
-
*
|
|
48
|
+
* Time interval (in milliseconds) at which all active client ID buckets are
|
|
49
|
+
* decreasing their counters, slowly freeing again their capacities. Once a
|
|
50
|
+
* bucket is empty again (i.e. its counter is back at zero), the bucket will
|
|
51
|
+
* be automatically removed.
|
|
24
52
|
*
|
|
25
|
-
* @defaultValue
|
|
53
|
+
* @defaultValue 1000
|
|
26
54
|
*/
|
|
27
|
-
|
|
55
|
+
leakInterval: number;
|
|
28
56
|
}
|
|
29
57
|
/**
|
|
30
58
|
* Creates a new {@link RateLimiter} interceptor.
|
|
@@ -33,41 +61,17 @@ export interface RateLimitOpts<T extends RequestCtx = RequestCtx> {
|
|
|
33
61
|
*/
|
|
34
62
|
export declare const rateLimiter: <T extends RequestCtx = RequestCtx>(opts: RateLimitOpts<T>) => RateLimiter<T>;
|
|
35
63
|
/**
|
|
36
|
-
* Configurable
|
|
64
|
+
* Configurable rate limiter interceptor using a counter-based leaky bucket
|
|
65
|
+
* algorithm to ensure both an average rate and allow temporary bursting (up to
|
|
66
|
+
* an implicitly defined limit).
|
|
67
|
+
*
|
|
68
|
+
*
|
|
37
69
|
*/
|
|
38
70
|
export declare class RateLimiter<T extends RequestCtx = RequestCtx> implements Interceptor<T> {
|
|
39
|
-
|
|
40
|
-
|
|
71
|
+
buckets: LeakyBucketMap<string>;
|
|
72
|
+
capacity: Fn2<T, string, number>;
|
|
41
73
|
clientID: Fn<T, Maybe<string>>;
|
|
42
|
-
|
|
43
|
-
constructor({ id, quota, period }: RateLimitOpts<T>);
|
|
74
|
+
constructor({ id, maxBuckets, bucketCapacity, leakInterval, }: RateLimitOpts<T>);
|
|
44
75
|
pre(ctx: T): boolean;
|
|
45
76
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Ring-buffer based history of request timestamps (per client).
|
|
48
|
-
*
|
|
49
|
-
* @remarks
|
|
50
|
-
* Based on thi.ng/buffers `FIFOBuffer` implementation.
|
|
51
|
-
*
|
|
52
|
-
* @internal
|
|
53
|
-
*/
|
|
54
|
-
declare class Timestamps {
|
|
55
|
-
quota: number;
|
|
56
|
-
buf: number[];
|
|
57
|
-
rpos: number;
|
|
58
|
-
wpos: number;
|
|
59
|
-
num: number;
|
|
60
|
-
constructor(quota: number);
|
|
61
|
-
/**
|
|
62
|
-
* Removes any timestamps older than `threshold` from the buffer and returns
|
|
63
|
-
* remaining number of timestamps.
|
|
64
|
-
*
|
|
65
|
-
* @param threshold
|
|
66
|
-
*/
|
|
67
|
-
expire(threshold: number): number;
|
|
68
|
-
peek(): number;
|
|
69
|
-
writable(): boolean;
|
|
70
|
-
write(x: number): boolean;
|
|
71
|
-
}
|
|
72
|
-
export {};
|
|
73
77
|
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -1,78 +1,28 @@
|
|
|
1
|
-
import { TLRUCache } from "@thi.ng/cache";
|
|
2
1
|
import { isNumber } from "@thi.ng/checks";
|
|
2
|
+
import { LeakyBucketMap } from "@thi.ng/leaky-bucket";
|
|
3
3
|
const rateLimiter = (opts) => new RateLimiter(opts);
|
|
4
4
|
class RateLimiter {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
buckets;
|
|
6
|
+
capacity;
|
|
7
7
|
clientID;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
constructor({
|
|
9
|
+
id,
|
|
10
|
+
maxBuckets,
|
|
11
|
+
bucketCapacity,
|
|
12
|
+
leakInterval
|
|
13
|
+
}) {
|
|
10
14
|
this.clientID = id ?? ((ctx) => ctx.req.socket.remoteAddress);
|
|
11
|
-
this.
|
|
12
|
-
this.
|
|
13
|
-
this.cache = new TLRUCache(null, {
|
|
14
|
-
ttl: this.period,
|
|
15
|
-
autoExtend: true
|
|
16
|
-
});
|
|
15
|
+
this.capacity = isNumber(bucketCapacity) ? () => bucketCapacity : bucketCapacity;
|
|
16
|
+
this.buckets = new LeakyBucketMap({ leakInterval, maxBuckets });
|
|
17
17
|
}
|
|
18
18
|
pre(ctx) {
|
|
19
|
-
const now = Date.now();
|
|
20
19
|
const id = this.clientID(ctx);
|
|
21
20
|
if (!id) return true;
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{},
|
|
27
|
-
`Try again @ ${new Date(
|
|
28
|
-
timestamps.peek() + this.period
|
|
29
|
-
).toISOString()}`
|
|
30
|
-
);
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
} else {
|
|
34
|
-
timestamps = new Timestamps(this.clientQuota(ctx, id));
|
|
35
|
-
this.cache.set(id, timestamps);
|
|
21
|
+
const cap = !this.buckets.has(id) ? this.capacity(ctx, id) : void 0;
|
|
22
|
+
if (!this.buckets.update(id, cap)) {
|
|
23
|
+
ctx.res.rateLimit({}, `Try again later.`);
|
|
24
|
+
return false;
|
|
36
25
|
}
|
|
37
|
-
timestamps.write(now);
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
class Timestamps {
|
|
42
|
-
constructor(quota) {
|
|
43
|
-
this.quota = quota;
|
|
44
|
-
}
|
|
45
|
-
buf = [];
|
|
46
|
-
rpos = 0;
|
|
47
|
-
wpos = 0;
|
|
48
|
-
num = 0;
|
|
49
|
-
/**
|
|
50
|
-
* Removes any timestamps older than `threshold` from the buffer and returns
|
|
51
|
-
* remaining number of timestamps.
|
|
52
|
-
*
|
|
53
|
-
* @param threshold
|
|
54
|
-
*/
|
|
55
|
-
expire(threshold) {
|
|
56
|
-
let { buf, rpos, num } = this;
|
|
57
|
-
const max = buf.length;
|
|
58
|
-
while (num && buf[rpos] < threshold) {
|
|
59
|
-
rpos = (rpos + 1) % max;
|
|
60
|
-
num--;
|
|
61
|
-
}
|
|
62
|
-
this.rpos = rpos;
|
|
63
|
-
return this.num = num;
|
|
64
|
-
}
|
|
65
|
-
peek() {
|
|
66
|
-
return this.buf[this.rpos];
|
|
67
|
-
}
|
|
68
|
-
writable() {
|
|
69
|
-
return this.num < this.quota;
|
|
70
|
-
}
|
|
71
|
-
write(x) {
|
|
72
|
-
const { buf, wpos } = this;
|
|
73
|
-
buf[wpos] = x;
|
|
74
|
-
this.wpos = (wpos + 1) % this.quota;
|
|
75
|
-
this.num++;
|
|
76
26
|
return true;
|
|
77
27
|
}
|
|
78
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thi.ng/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Minimal HTTP server with declarative routing, static file serving and freely extensible via pre/post interceptors",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -39,19 +39,20 @@
|
|
|
39
39
|
"tool:tangle": "../../node_modules/.bin/tangle src/**/*.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@thi.ng/api": "^8.11.
|
|
43
|
-
"@thi.ng/arrays": "^2.10.
|
|
44
|
-
"@thi.ng/cache": "^2.3.
|
|
45
|
-
"@thi.ng/checks": "^3.7.
|
|
46
|
-
"@thi.ng/errors": "^2.5.
|
|
47
|
-
"@thi.ng/file-io": "^2.1.
|
|
48
|
-
"@thi.ng/
|
|
49
|
-
"@thi.ng/
|
|
50
|
-
"@thi.ng/
|
|
51
|
-
"@thi.ng/
|
|
52
|
-
"@thi.ng/
|
|
53
|
-
"@thi.ng/
|
|
54
|
-
"@thi.ng/
|
|
42
|
+
"@thi.ng/api": "^8.11.22",
|
|
43
|
+
"@thi.ng/arrays": "^2.10.19",
|
|
44
|
+
"@thi.ng/cache": "^2.3.27",
|
|
45
|
+
"@thi.ng/checks": "^3.7.2",
|
|
46
|
+
"@thi.ng/errors": "^2.5.28",
|
|
47
|
+
"@thi.ng/file-io": "^2.1.31",
|
|
48
|
+
"@thi.ng/leaky-bucket": "^0.1.0",
|
|
49
|
+
"@thi.ng/logger": "^3.1.3",
|
|
50
|
+
"@thi.ng/mime": "^2.7.4",
|
|
51
|
+
"@thi.ng/paths": "^5.2.5",
|
|
52
|
+
"@thi.ng/router": "^4.1.22",
|
|
53
|
+
"@thi.ng/strings": "^3.9.7",
|
|
54
|
+
"@thi.ng/timestamp": "^1.1.7",
|
|
55
|
+
"@thi.ng/uuid": "^1.1.19"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@types/node": "^22.13.1",
|
|
@@ -163,5 +164,5 @@
|
|
|
163
164
|
"status": "alpha",
|
|
164
165
|
"year": 2024
|
|
165
166
|
},
|
|
166
|
-
"gitHead": "
|
|
167
|
+
"gitHead": "55987d581e4985b1c3091ba6be3f9b53a0c5eeea\n"
|
|
167
168
|
}
|