@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2025-02-21T21:54:17Z
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
  [![Mastodon Follow](https://img.shields.io/mastodon/follow/109331703950160316?domain=https%3A%2F%2Fmastodon.thi.ng&style=social)](https://mastodon.thi.ng/@toxi)
8
8
 
9
9
  > [!NOTE]
10
- > This is one of 202 standalone projects, maintained as part
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.28 KB
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 { TLRUCache } from "@thi.ng/cache";
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 and configured {@link RateLimitOpts.period}. If given as number, that
14
- * limit will be enforced for all identified clients.
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
- * Quotas are only computed whenever a new client ID is first encountered or
18
- * when its previous request records have expired from the cache (after
19
- * {@link RateLimitOpts.period} seconds).
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
- quota: number | Fn2<T, string, number>;
46
+ bucketCapacity: number | Fn2<T, string, number>;
22
47
  /**
23
- * Size of the time window (in secords) to which this rate limiter applies.
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 900
53
+ * @defaultValue 1000
26
54
  */
27
- period?: number;
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, sliding time window-based rate limiter interceptor.
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
- cache: TLRUCache<string, Timestamps>;
40
- clientQuota: Fn2<T, string, number>;
71
+ buckets: LeakyBucketMap<string>;
72
+ capacity: Fn2<T, string, number>;
41
73
  clientID: Fn<T, Maybe<string>>;
42
- period: number;
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
- cache;
6
- clientQuota;
5
+ buckets;
6
+ capacity;
7
7
  clientID;
8
- period;
9
- constructor({ id, quota, period = 15 * 60 }) {
8
+ constructor({
9
+ id,
10
+ maxBuckets,
11
+ bucketCapacity,
12
+ leakInterval
13
+ }) {
10
14
  this.clientID = id ?? ((ctx) => ctx.req.socket.remoteAddress);
11
- this.clientQuota = isNumber(quota) ? () => quota : quota;
12
- this.period = (period ?? 15 * 60) * 1e3;
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
- let timestamps = this.cache.get(id);
23
- if (timestamps) {
24
- if (timestamps.expire(now - this.period) >= timestamps.quota) {
25
- ctx.res.rateLimit(
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.6.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.21",
43
- "@thi.ng/arrays": "^2.10.17",
44
- "@thi.ng/cache": "^2.3.25",
45
- "@thi.ng/checks": "^3.7.0",
46
- "@thi.ng/errors": "^2.5.27",
47
- "@thi.ng/file-io": "^2.1.29",
48
- "@thi.ng/logger": "^3.1.2",
49
- "@thi.ng/mime": "^2.7.3",
50
- "@thi.ng/paths": "^5.2.3",
51
- "@thi.ng/router": "^4.1.20",
52
- "@thi.ng/strings": "^3.9.6",
53
- "@thi.ng/timestamp": "^1.1.6",
54
- "@thi.ng/uuid": "^1.1.18"
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": "2958e6a9881bd9e06de587a2141f6d23c64278db\n"
167
+ "gitHead": "55987d581e4985b1c3091ba6be3f9b53a0c5eeea\n"
167
168
  }