@voltro/plugin-ratelimit 0.85.0 → 0.86.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/dist/store.js ADDED
@@ -0,0 +1,29 @@
1
+ import { consumeBucket as e } from "./window.js";
2
+ import { Effect as t } from "effect";
3
+ //#region src/store.ts
4
+ var n = (n = {}) => {
5
+ let r = n.maxKeys ?? 1e5, i = n.staleAfterMs ?? 36e5, a = /* @__PURE__ */ new Map(), o = (e) => {
6
+ if (a.size <= r) return;
7
+ let t = a.size - r, n = 0;
8
+ for (let r of a.keys()) {
9
+ if (n >= t) break;
10
+ r !== e && (a.delete(r), n += 1);
11
+ }
12
+ };
13
+ return {
14
+ consume: (n, r, i) => t.sync(() => {
15
+ let t = a.get(n), { state: s, decision: c } = e(t?.state, r, i);
16
+ return a.set(n, {
17
+ state: s,
18
+ touched: i
19
+ }), o(n), c;
20
+ }),
21
+ sweep: (e) => {
22
+ let t = e - i;
23
+ for (let [e, n] of a) n.touched < t && a.delete(e);
24
+ },
25
+ size: () => a.size
26
+ };
27
+ };
28
+ //#endregion
29
+ export { n as memoryStore };
package/dist/window.js ADDED
@@ -0,0 +1,117 @@
1
+ //#region src/window.ts
2
+ var e = {
3
+ ms: 1,
4
+ s: 1e3,
5
+ m: 6e4,
6
+ h: 36e5,
7
+ d: 864e5
8
+ }, t = (t) => {
9
+ if (typeof t == "number") {
10
+ if (!Number.isFinite(t) || t <= 0) throw Error(`@voltro/plugin-ratelimit: window must be a positive number, got ${t}`);
11
+ return t;
12
+ }
13
+ let n = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/.exec(t.trim());
14
+ if (!n) throw Error(`@voltro/plugin-ratelimit: invalid window "${t}" — use e.g. "1m", "10s", "500ms", "2h", "1d"`);
15
+ return Number(n[1]) * e[n[2]];
16
+ }, n = (e, t, n) => {
17
+ switch (t.algorithm) {
18
+ case "fixed-window": return r(e, t, n);
19
+ case "sliding-window": return i(e, t, n);
20
+ case "token-bucket": return a(e, t, n);
21
+ }
22
+ }, r = (e, t, n) => {
23
+ let r = e?.kind === "fixed" ? e.windowStart : n, i = e?.kind === "fixed" ? e.count : 0;
24
+ n - r >= t.windowMs && (r = n, i = 0);
25
+ let a = r + t.windowMs;
26
+ return i >= t.limit ? {
27
+ state: {
28
+ kind: "fixed",
29
+ windowStart: r,
30
+ count: i
31
+ },
32
+ decision: {
33
+ allowed: !1,
34
+ remaining: 0,
35
+ resetAtMs: a,
36
+ retryAfterMs: a - n
37
+ }
38
+ } : (i += 1, {
39
+ state: {
40
+ kind: "fixed",
41
+ windowStart: r,
42
+ count: i
43
+ },
44
+ decision: {
45
+ allowed: !0,
46
+ remaining: t.limit - i,
47
+ resetAtMs: a,
48
+ retryAfterMs: 0
49
+ }
50
+ });
51
+ }, i = (e, t, n) => {
52
+ let r = t.windowMs, i = e?.kind === "sliding" ? e.currStart : n, a = e?.kind === "sliding" ? e.currCount : 0, o = e?.kind === "sliding" ? e.prevCount : 0, s = Math.floor((n - i) / r);
53
+ s >= 2 ? (o = 0, a = 0, i = n) : s === 1 && (o = a, a = 0, i += r);
54
+ let c = (r - (n - i)) / r, l = o * c + a, u = i + r;
55
+ return l >= t.limit ? {
56
+ state: {
57
+ kind: "sliding",
58
+ currStart: i,
59
+ currCount: a,
60
+ prevCount: o
61
+ },
62
+ decision: {
63
+ allowed: !1,
64
+ remaining: 0,
65
+ resetAtMs: u,
66
+ retryAfterMs: u - n
67
+ }
68
+ } : (a += 1, {
69
+ state: {
70
+ kind: "sliding",
71
+ currStart: i,
72
+ currCount: a,
73
+ prevCount: o
74
+ },
75
+ decision: {
76
+ allowed: !0,
77
+ remaining: Math.max(0, Math.floor(t.limit - l - 1)),
78
+ resetAtMs: u,
79
+ retryAfterMs: 0
80
+ }
81
+ });
82
+ }, a = (e, t, n) => {
83
+ let r = t.burst, i = t.limit / t.windowMs, a = e?.kind === "token" ? e.tokens : r, o = e?.kind === "token" ? e.lastRefill : n;
84
+ if (a = Math.min(r, a + (n - o) * i), a >= 1) {
85
+ --a;
86
+ let e = n + Math.ceil((r - a) / i);
87
+ return {
88
+ state: {
89
+ kind: "token",
90
+ tokens: a,
91
+ lastRefill: n
92
+ },
93
+ decision: {
94
+ allowed: !0,
95
+ remaining: Math.floor(a),
96
+ resetAtMs: e,
97
+ retryAfterMs: 0
98
+ }
99
+ };
100
+ }
101
+ let s = Math.ceil((1 - a) / i);
102
+ return {
103
+ state: {
104
+ kind: "token",
105
+ tokens: a,
106
+ lastRefill: n
107
+ },
108
+ decision: {
109
+ allowed: !1,
110
+ remaining: 0,
111
+ resetAtMs: n + s,
112
+ retryAfterMs: s
113
+ }
114
+ };
115
+ };
116
+ //#endregion
117
+ export { n as consumeBucket, t as parseWindow };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-ratelimit",
3
- "version": "0.85.0",
3
+ "version": "0.86.0",
4
4
  "description": "Rate-limiting plugin — per-endpoint, per-subject and per-tenant request limits via the RPC interceptors. Highly configurable rules (fixed-window / sliding-window / token-bucket), composite keying, static per-tenant overrides and a dynamic resolve() hook. Stores: memory (default, single-node), postgres + redis (multi-node, atomic).",
5
5
  "keywords": [
6
6
  "voltro",
@@ -48,10 +48,11 @@
48
48
  "node": ">=24.0.0"
49
49
  },
50
50
  "dependencies": {
51
- "@voltro/database": "0.85.0",
52
- "@voltro/kv": "0.85.0",
53
- "@voltro/logger": "0.85.0",
54
- "@voltro/protocol": "0.85.0"
51
+ "@voltro/database": "0.86.0",
52
+ "@voltro/kv": "0.86.0",
53
+ "@voltro/logger": "0.86.0",
54
+ "@voltro/protocol": "0.86.0",
55
+ "@voltro/runtime": "0.86.0"
55
56
  },
56
57
  "peerDependencies": {
57
58
  "effect": "4.0.1"
@@ -1,160 +0,0 @@
1
- import { Effect as e } from "effect";
2
- import { dropped as t, eq as n, id as r, json as i, lt as a, queryFor as o, table as s, text as c, timestamp as l } from "@voltro/database";
3
- //#region src/window.ts
4
- var u = {
5
- ms: 1,
6
- s: 1e3,
7
- m: 6e4,
8
- h: 36e5,
9
- d: 864e5
10
- }, d = (e) => {
11
- if (typeof e == "number") {
12
- if (!Number.isFinite(e) || e <= 0) throw Error(`@voltro/plugin-ratelimit: window must be a positive number, got ${e}`);
13
- return e;
14
- }
15
- let t = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/.exec(e.trim());
16
- if (!t) throw Error(`@voltro/plugin-ratelimit: invalid window "${e}" — use e.g. "1m", "10s", "500ms", "2h", "1d"`);
17
- return Number(t[1]) * u[t[2]];
18
- }, f = (e, t, n) => {
19
- switch (t.algorithm) {
20
- case "fixed-window": return p(e, t, n);
21
- case "sliding-window": return m(e, t, n);
22
- case "token-bucket": return h(e, t, n);
23
- }
24
- }, p = (e, t, n) => {
25
- let r = e?.kind === "fixed" ? e.windowStart : n, i = e?.kind === "fixed" ? e.count : 0;
26
- n - r >= t.windowMs && (r = n, i = 0);
27
- let a = r + t.windowMs;
28
- return i >= t.limit ? {
29
- state: {
30
- kind: "fixed",
31
- windowStart: r,
32
- count: i
33
- },
34
- decision: {
35
- allowed: !1,
36
- remaining: 0,
37
- resetAtMs: a,
38
- retryAfterMs: a - n
39
- }
40
- } : (i += 1, {
41
- state: {
42
- kind: "fixed",
43
- windowStart: r,
44
- count: i
45
- },
46
- decision: {
47
- allowed: !0,
48
- remaining: t.limit - i,
49
- resetAtMs: a,
50
- retryAfterMs: 0
51
- }
52
- });
53
- }, m = (e, t, n) => {
54
- let r = t.windowMs, i = e?.kind === "sliding" ? e.currStart : n, a = e?.kind === "sliding" ? e.currCount : 0, o = e?.kind === "sliding" ? e.prevCount : 0, s = Math.floor((n - i) / r);
55
- s >= 2 ? (o = 0, a = 0, i = n) : s === 1 && (o = a, a = 0, i += r);
56
- let c = (r - (n - i)) / r, l = o * c + a, u = i + r;
57
- return l >= t.limit ? {
58
- state: {
59
- kind: "sliding",
60
- currStart: i,
61
- currCount: a,
62
- prevCount: o
63
- },
64
- decision: {
65
- allowed: !1,
66
- remaining: 0,
67
- resetAtMs: u,
68
- retryAfterMs: u - n
69
- }
70
- } : (a += 1, {
71
- state: {
72
- kind: "sliding",
73
- currStart: i,
74
- currCount: a,
75
- prevCount: o
76
- },
77
- decision: {
78
- allowed: !0,
79
- remaining: Math.max(0, Math.floor(t.limit - l - 1)),
80
- resetAtMs: u,
81
- retryAfterMs: 0
82
- }
83
- });
84
- }, h = (e, t, n) => {
85
- let r = t.burst, i = t.limit / t.windowMs, a = e?.kind === "token" ? e.tokens : r, o = e?.kind === "token" ? e.lastRefill : n;
86
- if (a = Math.min(r, a + (n - o) * i), a >= 1) {
87
- --a;
88
- let e = n + Math.ceil((r - a) / i);
89
- return {
90
- state: {
91
- kind: "token",
92
- tokens: a,
93
- lastRefill: n
94
- },
95
- decision: {
96
- allowed: !0,
97
- remaining: Math.floor(a),
98
- resetAtMs: e,
99
- retryAfterMs: 0
100
- }
101
- };
102
- }
103
- let s = Math.ceil((1 - a) / i);
104
- return {
105
- state: {
106
- kind: "token",
107
- tokens: a,
108
- lastRefill: n
109
- },
110
- decision: {
111
- allowed: !1,
112
- remaining: 0,
113
- resetAtMs: n + s,
114
- retryAfterMs: s
115
- }
116
- };
117
- }, g = "_voltro_ratelimit", _ = s(g, {
118
- id: r({ scheme: "numeric" }),
119
- key: c().maxLength(512),
120
- state: i().nullable(),
121
- touchedAt: l().default("now"),
122
- touched: t()
123
- }).unique("rateLimitKey", ["key"]).nonReactive(), v = (t = {}) => {
124
- let r = t.staleAfterMs ?? 36e5, i = null, s = (e, t, n) => ({
125
- failOpenReason: n,
126
- allowed: !0,
127
- remaining: e.limit,
128
- resetAtMs: t + e.windowMs,
129
- retryAfterMs: 0
130
- }), c = async (e, t, r, i) => {
131
- let a = new Date(i);
132
- return await e.insertIgnore(g, {
133
- key: t,
134
- state: null,
135
- touchedAt: a
136
- }, { conflictColumns: ["key"] }), e.transactional(async (e) => {
137
- let { state: s, decision: c } = f((await e.query(o(_).where(n("key", t)).forUpdate().descriptor))[0]?.state ?? void 0 ?? void 0, r, i);
138
- return await e.updateMany(g, {
139
- state: s,
140
- touchedAt: a
141
- }, { where: n("key", t) }), c;
142
- });
143
- };
144
- return {
145
- consume: (t, n, r) => {
146
- let a = i;
147
- return a ? e.tryPromise(() => c(a, t, n, r)).pipe(e.catch(() => e.succeed(s(n, r, "store-error")))) : e.succeed(s(n, r, "unbound"));
148
- },
149
- sweep: (e) => {
150
- let t = i;
151
- t && t.deleteMany(g, { where: a("touchedAt", new Date(e - r)) }).catch(() => {});
152
- },
153
- size: () => -1,
154
- bind: async (e) => {
155
- i = e;
156
- }
157
- };
158
- };
159
- //#endregion
160
- export { d as a, f as i, v as n, _ as r, g as t };