@voltro/plugin-ratelimit 0.81.0 → 0.83.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.
@@ -1,17 +1,37 @@
1
+ import { ColumnBuilder } from '@voltro/database';
2
+ import { DataStore } from '@voltro/database';
1
3
  import { Effect } from 'effect';
2
- import { SqlClient } from '@effect/sql';
4
+ import { FieldDefinitions } from '@voltro/database';
5
+ import { Table } from '@voltro/database';
3
6
 
4
7
  declare type Algorithm = 'fixed-window' | 'sliding-window' | 'token-bucket';
5
8
 
9
+ /** Opaque per-key state. The store persists it verbatim; only this
10
+ * module reads its shape. */
11
+ declare type BucketState = {
12
+ readonly kind: 'fixed';
13
+ readonly windowStart: number;
14
+ readonly count: number;
15
+ } | {
16
+ readonly kind: 'sliding';
17
+ readonly currStart: number;
18
+ readonly currCount: number;
19
+ readonly prevCount: number;
20
+ } | {
21
+ readonly kind: 'token';
22
+ readonly tokens: number;
23
+ readonly lastRefill: number;
24
+ };
25
+
6
26
  export declare const postgresStore: (options?: PostgresStoreOptions) => RateLimitStore;
7
27
 
8
28
  export declare interface PostgresStoreOptions {
9
- /** Table name. Default `_voltro_ratelimit`. */
10
- readonly table?: string;
11
29
  /** Drop rows untouched for longer than this (ms) on sweep. Default 1h. */
12
30
  readonly staleAfterMs?: number;
13
31
  }
14
32
 
33
+ export declare const RATELIMIT_TABLE = "_voltro_ratelimit";
34
+
15
35
  declare interface RateLimitDecision {
16
36
  readonly allowed: boolean;
17
37
  /** The request passed without a quota check because the store was unavailable.
@@ -38,13 +58,22 @@ declare interface RateLimitStore {
38
58
  readonly activate?: (env: NodeJS.ProcessEnv) => Promise<void>;
39
59
  /** Optional teardown at plugin deactivate (close pools). */
40
60
  readonly deactivate?: () => Promise<void>;
41
- /** Optional bind of the framework's already-open `SqlClient` (from the
42
- * plugin's `bindDataStore(store, ctx)` → `ctx.sql`). A SQL-backed store
43
- * runs through THIS client instead of standing up its own pool; creates
44
- * its bookkeeping table here. Not present on memory / redis stores. */
45
- readonly bindSql?: (sql: SqlClient.SqlClient) => Promise<void>;
61
+ /** Optional bind of the app's `DataStore` (the plugin's `bindDataStore`).
62
+ * A store kept in the app's database reads and writes through it — its
63
+ * table is declared through `extendSchema`, never created here. Not
64
+ * present on memory / redis stores. */
65
+ readonly bind?: (store: DataStore) => Promise<void>;
46
66
  }
47
67
 
68
+ /** One row per rate-limit key. `id` + a unique `key` rather than `key` as the
69
+ * primary key: every dialect store addresses a row by `id`. */
70
+ export declare const rateLimitTable: Table<"_voltro_ratelimit", FieldDefinitions<{
71
+ readonly id: ColumnBuilder<number, "id", boolean>;
72
+ readonly key: ColumnBuilder<string, "text", boolean>;
73
+ readonly state: ColumnBuilder<BucketState | null, "json", boolean>;
74
+ readonly touchedAt: ColumnBuilder<Date, "timestamp", boolean>;
75
+ }>, false, never>;
76
+
48
77
  /** The fully-resolved limit the store consumes against. All dynamic /
49
78
  * per-tenant resolution has already collapsed into these scalars. */
50
79
  declare interface ResolvedLimit {
@@ -1,51 +1,2 @@
1
- import { t as e } from "./window-lyQ6LXhj.js";
2
- import { Effect as t } from "effect";
3
- import { SqlClient as n } from "@effect/sql";
4
- //#region src/postgresStore.ts
5
- var r = (r = {}) => {
6
- let i = r.table ?? "_voltro_ratelimit", a = r.staleAfterMs ?? 36e5, o = null, s = (e) => t.runPromise(t.provideService(e, n.SqlClient, o)), c = (e, t, n) => ({
7
- failOpenReason: n,
8
- allowed: !0,
9
- remaining: e.limit,
10
- resetAtMs: t + e.windowMs,
11
- retryAfterMs: 0
12
- });
13
- return {
14
- consume: (r, a, l) => {
15
- if (!o) return t.succeed(c(a, l, "unbound"));
16
- let u = t.gen(function* () {
17
- let o = yield* n.SqlClient;
18
- return yield* o.withTransaction(t.gen(function* () {
19
- yield* o`SELECT pg_advisory_xact_lock(hashtextextended(${r}, 0))`;
20
- let t = (yield* o`
21
- SELECT state FROM ${o(i)} WHERE key = ${r} FOR UPDATE`)[0]?.state, { state: n, decision: s } = e(t, a, l), c = JSON.stringify(n);
22
- return yield* o`
23
- INSERT INTO ${o(i)} (key, state, touched)
24
- VALUES (${r}, ${c}::jsonb, ${l})
25
- ON CONFLICT (key) DO UPDATE SET state = ${c}::jsonb, touched = ${l}`, s;
26
- }));
27
- });
28
- return t.tryPromise(() => s(u)).pipe(t.catchAll(() => t.succeed(c(a, l, "store-error"))));
29
- },
30
- sweep: (e) => {
31
- o && s(t.gen(function* () {
32
- let t = yield* n.SqlClient;
33
- yield* t`DELETE FROM ${t(i)} WHERE touched < ${e - a}`;
34
- })).catch(() => {});
35
- },
36
- size: () => -1,
37
- bindSql: async (e) => {
38
- await t.runPromise(t.provideService(t.gen(function* () {
39
- let e = yield* n.SqlClient;
40
- yield* e`
41
- CREATE TABLE IF NOT EXISTS ${e(i)} (
42
- key TEXT PRIMARY KEY,
43
- state JSONB NOT NULL,
44
- touched BIGINT NOT NULL
45
- )`;
46
- }), n.SqlClient, e)), o = e;
47
- }
48
- };
49
- };
50
- //#endregion
51
- export { r as postgresStore };
1
+ import { n as e, r as t, t as n } from "./postgresStore-B5gTBWHd.js";
2
+ export { n as RATELIMIT_TABLE, e as postgresStore, t as rateLimitTable };
@@ -1,6 +1,6 @@
1
+ import { DataStore } from '@voltro/database';
1
2
  import { Effect } from 'effect';
2
3
  import { RespClient } from '@voltro/kv/connection';
3
- import { SqlClient } from '@effect/sql';
4
4
 
5
5
  declare type Algorithm = 'fixed-window' | 'sliding-window' | 'token-bucket';
6
6
 
@@ -30,11 +30,11 @@ declare interface RateLimitStore {
30
30
  readonly activate?: (env: NodeJS.ProcessEnv) => Promise<void>;
31
31
  /** Optional teardown at plugin deactivate (close pools). */
32
32
  readonly deactivate?: () => Promise<void>;
33
- /** Optional bind of the framework's already-open `SqlClient` (from the
34
- * plugin's `bindDataStore(store, ctx)` → `ctx.sql`). A SQL-backed store
35
- * runs through THIS client instead of standing up its own pool; creates
36
- * its bookkeeping table here. Not present on memory / redis stores. */
37
- readonly bindSql?: (sql: SqlClient.SqlClient) => Promise<void>;
33
+ /** Optional bind of the app's `DataStore` (the plugin's `bindDataStore`).
34
+ * A store kept in the app's database reads and writes through it — its
35
+ * table is declared through `extendSchema`, never created here. Not
36
+ * present on memory / redis stores. */
37
+ readonly bind?: (store: DataStore) => Promise<void>;
38
38
  }
39
39
 
40
40
  export declare const redisStore: (options?: RedisStoreOptions) => RateLimitStore;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-ratelimit",
3
- "version": "0.81.0",
3
+ "version": "0.83.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,9 +48,10 @@
48
48
  "node": ">=24.0.0"
49
49
  },
50
50
  "dependencies": {
51
- "@voltro/kv": "0.81.0",
52
- "@voltro/logger": "0.81.0",
53
- "@voltro/protocol": "0.81.0"
51
+ "@voltro/database": "0.83.0",
52
+ "@voltro/kv": "0.83.0",
53
+ "@voltro/logger": "0.83.0",
54
+ "@voltro/protocol": "0.83.0"
54
55
  },
55
56
  "optionalDependencies": {
56
57
  "@effect/sql": "^0.52.1"
@@ -1,117 +0,0 @@
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 { t as n, n as t };