@xyph3r/rate-limiter 0.1.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/LICENSE +21 -0
- package/README.md +310 -0
- package/dist/adapters/express.d.ts +26 -0
- package/dist/adapters/express.d.ts.map +1 -0
- package/dist/adapters/express.js +51 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/fastify.d.ts +24 -0
- package/dist/adapters/fastify.d.ts.map +1 -0
- package/dist/adapters/fastify.js +40 -0
- package/dist/adapters/fastify.js.map +1 -0
- package/dist/adapters/fetch.d.ts +17 -0
- package/dist/adapters/fetch.d.ts.map +1 -0
- package/dist/adapters/fetch.js +53 -0
- package/dist/adapters/fetch.js.map +1 -0
- package/dist/adapters/hono.d.ts +25 -0
- package/dist/adapters/hono.d.ts.map +1 -0
- package/dist/adapters/hono.js +40 -0
- package/dist/adapters/hono.js.map +1 -0
- package/dist/adapters/nest.d.ts +32 -0
- package/dist/adapters/nest.d.ts.map +1 -0
- package/dist/adapters/nest.js +47 -0
- package/dist/adapters/nest.js.map +1 -0
- package/dist/adapters/next.d.ts +12 -0
- package/dist/adapters/next.d.ts.map +1 -0
- package/dist/adapters/next.js +11 -0
- package/dist/adapters/next.js.map +1 -0
- package/dist/core/cached-rate-limiter-proxy.d.ts +17 -0
- package/dist/core/cached-rate-limiter-proxy.d.ts.map +1 -0
- package/dist/core/cached-rate-limiter-proxy.js +47 -0
- package/dist/core/cached-rate-limiter-proxy.js.map +1 -0
- package/dist/core/create-rate-limiter.d.ts +11 -0
- package/dist/core/create-rate-limiter.d.ts.map +1 -0
- package/dist/core/create-rate-limiter.js +34 -0
- package/dist/core/create-rate-limiter.js.map +1 -0
- package/dist/core/rate-limiter-builder.d.ts +27 -0
- package/dist/core/rate-limiter-builder.d.ts.map +1 -0
- package/dist/core/rate-limiter-builder.js +73 -0
- package/dist/core/rate-limiter-builder.js.map +1 -0
- package/dist/core/rate-limiter.d.ts +23 -0
- package/dist/core/rate-limiter.d.ts.map +1 -0
- package/dist/core/rate-limiter.js +59 -0
- package/dist/core/rate-limiter.js.map +1 -0
- package/dist/core/strategy-factory.d.ts +18 -0
- package/dist/core/strategy-factory.d.ts.map +1 -0
- package/dist/core/strategy-factory.js +17 -0
- package/dist/core/strategy-factory.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/stores/memory-store.d.ts +9 -0
- package/dist/stores/memory-store.d.ts.map +1 -0
- package/dist/stores/memory-store.js +24 -0
- package/dist/stores/memory-store.js.map +1 -0
- package/dist/stores/rate-limit-store.d.ts +6 -0
- package/dist/stores/rate-limit-store.d.ts.map +1 -0
- package/dist/stores/rate-limit-store.js +2 -0
- package/dist/stores/rate-limit-store.js.map +1 -0
- package/dist/stores/redis-store.d.ts +26 -0
- package/dist/stores/redis-store.d.ts.map +1 -0
- package/dist/stores/redis-store.js +41 -0
- package/dist/stores/redis-store.js.map +1 -0
- package/dist/strategies/sliding-window-strategy.d.ts +31 -0
- package/dist/strategies/sliding-window-strategy.d.ts.map +1 -0
- package/dist/strategies/sliding-window-strategy.js +212 -0
- package/dist/strategies/sliding-window-strategy.js.map +1 -0
- package/dist/strategies/token-bucket-strategy.d.ts +30 -0
- package/dist/strategies/token-bucket-strategy.d.ts.map +1 -0
- package/dist/strategies/token-bucket-strategy.js +154 -0
- package/dist/strategies/token-bucket-strategy.js.map +1 -0
- package/dist/types.d.ts +48 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/headers.d.ts +4 -0
- package/dist/utils/headers.d.ts.map +1 -0
- package/dist/utils/headers.js +16 -0
- package/dist/utils/headers.js.map +1 -0
- package/dist/utils/http.d.ts +11 -0
- package/dist/utils/http.d.ts.map +1 -0
- package/dist/utils/http.js +41 -0
- package/dist/utils/http.js.map +1 -0
- package/dist/utils/math.d.ts +4 -0
- package/dist/utils/math.d.ts.map +1 -0
- package/dist/utils/math.js +21 -0
- package/dist/utils/math.js.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class MemoryStore {
|
|
2
|
+
entries = new Map();
|
|
3
|
+
async consume(key, strategy, context) {
|
|
4
|
+
this.pruneExpired(key, context.now);
|
|
5
|
+
const current = this.entries.get(key);
|
|
6
|
+
const snapshot = strategy.evaluate(current?.state, context);
|
|
7
|
+
const expiresAt = context.now + snapshot.ttlMs;
|
|
8
|
+
this.entries.set(key, {
|
|
9
|
+
expiresAt,
|
|
10
|
+
state: snapshot.state,
|
|
11
|
+
});
|
|
12
|
+
return snapshot;
|
|
13
|
+
}
|
|
14
|
+
async reset(key) {
|
|
15
|
+
this.entries.delete(key);
|
|
16
|
+
}
|
|
17
|
+
pruneExpired(key, now) {
|
|
18
|
+
const current = this.entries.get(key);
|
|
19
|
+
if (current && current.expiresAt <= now) {
|
|
20
|
+
this.entries.delete(key);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=memory-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-store.js","sourceRoot":"","sources":["../../src/stores/memory-store.ts"],"names":[],"mappings":"AAYA,MAAM,OAAO,WAAW;IACL,OAAO,GAAG,IAAI,GAAG,EAAgC,CAAC;IAEnE,KAAK,CAAC,OAAO,CACX,GAAW,EACX,QAAmC,EACnC,OAAiC;QAEjC,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QAEpC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAoC,CAAC;QACzE,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;QAC5D,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC;QAE/C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE;YACpB,SAAS;YACT,KAAK,EAAE,QAAQ,CAAC,KAAK;SACtB,CAAC,CAAC;QAEH,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,GAAW;QACrB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAEO,YAAY,CAAC,GAAW,EAAE,GAAW;QAC3C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,OAAO,IAAI,OAAO,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC;YACxC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RateLimitStrategy, StrategyExecutionContext, StrategyExecutionSnapshot } from "../types.js";
|
|
2
|
+
export interface RateLimitStore {
|
|
3
|
+
consume<TState>(key: string, strategy: RateLimitStrategy<TState>, context: StrategyExecutionContext): Promise<StrategyExecutionSnapshot<TState>>;
|
|
4
|
+
reset(key: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=rate-limit-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit-store.d.ts","sourceRoot":"","sources":["../../src/stores/rate-limit-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,iBAAiB,CAAC,MAAM,CAAC,EACnC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,yBAAyB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit-store.js","sourceRoot":"","sources":["../../src/stores/rate-limit-store.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RateLimitStrategy, StrategyExecutionContext, StrategyExecutionSnapshot } from "../types.js";
|
|
2
|
+
import type { RateLimitStore } from "./rate-limit-store.js";
|
|
3
|
+
export interface RedisCommandExecutor {
|
|
4
|
+
del(key: string): Promise<void>;
|
|
5
|
+
eval(script: string, key: string, args: string[]): Promise<unknown>;
|
|
6
|
+
}
|
|
7
|
+
export interface NodeRedisLikeClient {
|
|
8
|
+
del(key: string): Promise<unknown>;
|
|
9
|
+
eval(script: string, options: {
|
|
10
|
+
keys: string[];
|
|
11
|
+
arguments: string[];
|
|
12
|
+
}): Promise<unknown>;
|
|
13
|
+
}
|
|
14
|
+
export interface IORedisLikeClient {
|
|
15
|
+
del(key: string): Promise<unknown>;
|
|
16
|
+
eval(script: string, numKeys: number, ...args: string[]): Promise<unknown>;
|
|
17
|
+
}
|
|
18
|
+
export declare class RedisStore implements RateLimitStore {
|
|
19
|
+
private readonly executor;
|
|
20
|
+
constructor(executor: RedisCommandExecutor);
|
|
21
|
+
consume<TState>(key: string, strategy: RateLimitStrategy<TState>, context: StrategyExecutionContext): Promise<StrategyExecutionSnapshot<TState>>;
|
|
22
|
+
reset(key: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare function createNodeRedisExecutor(client: NodeRedisLikeClient): RedisCommandExecutor;
|
|
25
|
+
export declare function createIORedisExecutor(client: IORedisLikeClient): RedisCommandExecutor;
|
|
26
|
+
//# sourceMappingURL=redis-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-store.d.ts","sourceRoot":"","sources":["../../src/stores/redis-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,iBAAiB,EACjB,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,MAAM,WAAW,oBAAoB;IACnC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACrE;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,CACF,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,GAC/C,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5E;AAED,qBAAa,UAAW,YAAW,cAAc;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,oBAAoB;IAErD,OAAO,CAAC,MAAM,EAClB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,iBAAiB,CAAC,MAAM,CAAC,EACnC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,yBAAyB,CAAC,MAAM,CAAC,CAAC;IAgBvC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGxC;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,mBAAmB,GAC1B,oBAAoB,CAYtB;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,iBAAiB,GACxB,oBAAoB,CAStB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { UnsupportedStoreError } from "../errors.js";
|
|
2
|
+
export class RedisStore {
|
|
3
|
+
executor;
|
|
4
|
+
constructor(executor) {
|
|
5
|
+
this.executor = executor;
|
|
6
|
+
}
|
|
7
|
+
async consume(key, strategy, context) {
|
|
8
|
+
if (!strategy.redis) {
|
|
9
|
+
throw new UnsupportedStoreError(`Strategy "${strategy.kind}" does not provide a Redis program.`);
|
|
10
|
+
}
|
|
11
|
+
const raw = await this.executor.eval(strategy.redis.script, key, strategy.redis.getArgs(context));
|
|
12
|
+
return strategy.redis.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
async reset(key) {
|
|
15
|
+
await this.executor.del(key);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function createNodeRedisExecutor(client) {
|
|
19
|
+
return {
|
|
20
|
+
async del(key) {
|
|
21
|
+
await client.del(key);
|
|
22
|
+
},
|
|
23
|
+
async eval(script, key, args) {
|
|
24
|
+
return client.eval(script, {
|
|
25
|
+
keys: [key],
|
|
26
|
+
arguments: args,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function createIORedisExecutor(client) {
|
|
32
|
+
return {
|
|
33
|
+
async del(key) {
|
|
34
|
+
await client.del(key);
|
|
35
|
+
},
|
|
36
|
+
async eval(script, key, args) {
|
|
37
|
+
return client.eval(script, 1, key, ...args);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=redis-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-store.js","sourceRoot":"","sources":["../../src/stores/redis-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AA0BrD,MAAM,OAAO,UAAU;IACQ;IAA7B,YAA6B,QAA8B;QAA9B,aAAQ,GAAR,QAAQ,CAAsB;IAAG,CAAC;IAE/D,KAAK,CAAC,OAAO,CACX,GAAW,EACX,QAAmC,EACnC,OAAiC;QAEjC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACpB,MAAM,IAAI,qBAAqB,CAC7B,aAAa,QAAQ,CAAC,IAAI,qCAAqC,CAChE,CAAC;QACJ,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAClC,QAAQ,CAAC,KAAK,CAAC,MAAM,EACrB,GAAG,EACH,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAChC,CAAC;QAEF,OAAO,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,GAAW;QACrB,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;CACF;AAED,MAAM,UAAU,uBAAuB,CACrC,MAA2B;IAE3B,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAW;YACnB,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,MAAc,EAAE,GAAW,EAAE,IAAc;YACpD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE;gBACzB,IAAI,EAAE,CAAC,GAAG,CAAC;gBACX,SAAS,EAAE,IAAI;aAChB,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,MAAyB;IAEzB,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAW;YACnB,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,MAAc,EAAE,GAAW,EAAE,IAAc;YACpD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC9C,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RateLimitStrategy, RedisStrategyProgram, StrategyExecutionContext, StrategyExecutionSnapshot } from "../types.js";
|
|
2
|
+
export interface SlidingWindowConfig {
|
|
3
|
+
limit: number;
|
|
4
|
+
windowMs: number;
|
|
5
|
+
}
|
|
6
|
+
export interface SlidingWindowState {
|
|
7
|
+
currentCount: number;
|
|
8
|
+
currentWindowStart: number;
|
|
9
|
+
previousCount: number;
|
|
10
|
+
previousWindowStart: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Pattern: Strategy
|
|
14
|
+
* Problem: Sliding window and token bucket use materially different accounting logic.
|
|
15
|
+
* Solution: Both implement the same strategy contract, so the limiter delegates without branching.
|
|
16
|
+
* Trade-off: One extra layer of indirection; justified because these algorithms diverge quickly.
|
|
17
|
+
*/
|
|
18
|
+
export declare class SlidingWindowStrategy implements RateLimitStrategy<SlidingWindowState> {
|
|
19
|
+
readonly kind = "sliding-window";
|
|
20
|
+
readonly limit: number;
|
|
21
|
+
readonly policy: string;
|
|
22
|
+
readonly redis: RedisStrategyProgram<SlidingWindowState>;
|
|
23
|
+
private readonly windowMs;
|
|
24
|
+
constructor(config: SlidingWindowConfig);
|
|
25
|
+
initialState(now: number): SlidingWindowState;
|
|
26
|
+
evaluate(currentState: SlidingWindowState | undefined, context: StrategyExecutionContext): StrategyExecutionSnapshot<SlidingWindowState>;
|
|
27
|
+
private advanceState;
|
|
28
|
+
private calculateRetryAfterMs;
|
|
29
|
+
private getWindowStart;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=sliding-window-strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sliding-window-strategy.d.ts","sourceRoot":"","sources":["../../src/strategies/sliding-window-strategy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;GAKG;AACH,qBAAa,qBACX,YAAW,iBAAiB,CAAC,kBAAkB,CAAC;IAEhD,QAAQ,CAAC,IAAI,oBAAoB;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC,kBAAkB,CAAC,CAAC;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,MAAM,EAAE,mBAAmB;IAmBvC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB;IAU7C,QAAQ,CACN,YAAY,EAAE,kBAAkB,GAAG,SAAS,EAC5C,OAAO,EAAE,wBAAwB,GAChC,yBAAyB,CAAC,kBAAkB,CAAC;IAiChD,OAAO,CAAC,YAAY;IAyBpB,OAAO,CAAC,qBAAqB;IAoB7B,OAAO,CAAC,cAAc;CAGvB"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { RateLimiterConfigurationError } from "../errors.js";
|
|
2
|
+
import { clamp, normalizeRawRedisResult, toWholeNumber } from "../utils/math.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pattern: Strategy
|
|
5
|
+
* Problem: Sliding window and token bucket use materially different accounting logic.
|
|
6
|
+
* Solution: Both implement the same strategy contract, so the limiter delegates without branching.
|
|
7
|
+
* Trade-off: One extra layer of indirection; justified because these algorithms diverge quickly.
|
|
8
|
+
*/
|
|
9
|
+
export class SlidingWindowStrategy {
|
|
10
|
+
kind = "sliding-window";
|
|
11
|
+
limit;
|
|
12
|
+
policy;
|
|
13
|
+
redis;
|
|
14
|
+
windowMs;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
if (!Number.isFinite(config.limit) || config.limit <= 0) {
|
|
17
|
+
throw new RateLimiterConfigurationError("SlidingWindowStrategy requires a positive limit.");
|
|
18
|
+
}
|
|
19
|
+
if (!Number.isFinite(config.windowMs) || config.windowMs <= 0) {
|
|
20
|
+
throw new RateLimiterConfigurationError("SlidingWindowStrategy requires a positive windowMs.");
|
|
21
|
+
}
|
|
22
|
+
this.limit = Math.floor(config.limit);
|
|
23
|
+
this.windowMs = Math.floor(config.windowMs);
|
|
24
|
+
this.policy = `${this.limit};w=${Math.ceil(this.windowMs / 1000)}`;
|
|
25
|
+
this.redis = createSlidingWindowRedisProgram(this.limit, this.windowMs);
|
|
26
|
+
}
|
|
27
|
+
initialState(now) {
|
|
28
|
+
const windowStart = this.getWindowStart(now);
|
|
29
|
+
return {
|
|
30
|
+
currentCount: 0,
|
|
31
|
+
currentWindowStart: windowStart,
|
|
32
|
+
previousCount: 0,
|
|
33
|
+
previousWindowStart: windowStart - this.windowMs,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
evaluate(currentState, context) {
|
|
37
|
+
const state = this.advanceState(currentState, context.now);
|
|
38
|
+
const elapsed = context.now - state.currentWindowStart;
|
|
39
|
+
const previousWeight = clamp((this.windowMs - elapsed) / this.windowMs, 0, 1);
|
|
40
|
+
const effectiveBefore = state.currentCount + state.previousCount * previousWeight;
|
|
41
|
+
const allowed = effectiveBefore + context.cost <= this.limit + 1e-9;
|
|
42
|
+
if (allowed) {
|
|
43
|
+
state.currentCount += context.cost;
|
|
44
|
+
}
|
|
45
|
+
const effectiveAfter = state.currentCount + state.previousCount * previousWeight;
|
|
46
|
+
const resetAt = state.currentWindowStart + this.windowMs;
|
|
47
|
+
return {
|
|
48
|
+
state,
|
|
49
|
+
allowed,
|
|
50
|
+
used: this.limit - toWholeNumber(this.limit - effectiveAfter),
|
|
51
|
+
remaining: toWholeNumber(this.limit - effectiveAfter),
|
|
52
|
+
resetAt,
|
|
53
|
+
retryAfterMs: allowed
|
|
54
|
+
? 0
|
|
55
|
+
: this.calculateRetryAfterMs(state, context.cost, elapsed),
|
|
56
|
+
ttlMs: this.windowMs * 2,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
advanceState(currentState, now) {
|
|
60
|
+
if (!currentState) {
|
|
61
|
+
return this.initialState(now);
|
|
62
|
+
}
|
|
63
|
+
const nextWindowStart = this.getWindowStart(now);
|
|
64
|
+
if (currentState.currentWindowStart === nextWindowStart) {
|
|
65
|
+
return { ...currentState };
|
|
66
|
+
}
|
|
67
|
+
if (currentState.currentWindowStart === nextWindowStart - this.windowMs) {
|
|
68
|
+
return {
|
|
69
|
+
currentCount: 0,
|
|
70
|
+
currentWindowStart: nextWindowStart,
|
|
71
|
+
previousCount: currentState.currentCount,
|
|
72
|
+
previousWindowStart: currentState.currentWindowStart,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return this.initialState(now);
|
|
76
|
+
}
|
|
77
|
+
calculateRetryAfterMs(state, cost, elapsed) {
|
|
78
|
+
const resetAt = state.currentWindowStart + this.windowMs;
|
|
79
|
+
if (state.currentCount + cost > this.limit || state.previousCount <= 0) {
|
|
80
|
+
return Math.max(1, resetAt - (state.currentWindowStart + elapsed));
|
|
81
|
+
}
|
|
82
|
+
const decayBudget = this.limit - cost - state.currentCount;
|
|
83
|
+
if (decayBudget < 0) {
|
|
84
|
+
return Math.max(1, resetAt - (state.currentWindowStart + elapsed));
|
|
85
|
+
}
|
|
86
|
+
const targetWeight = clamp(decayBudget / state.previousCount, 0, 1);
|
|
87
|
+
const targetElapsed = this.windowMs - targetWeight * this.windowMs;
|
|
88
|
+
return Math.max(1, Math.ceil(targetElapsed - elapsed));
|
|
89
|
+
}
|
|
90
|
+
getWindowStart(now) {
|
|
91
|
+
return Math.floor(now / this.windowMs) * this.windowMs;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function createSlidingWindowRedisProgram(limit, windowMs) {
|
|
95
|
+
const script = `
|
|
96
|
+
local key = KEYS[1]
|
|
97
|
+
local now = tonumber(ARGV[1])
|
|
98
|
+
local cost = tonumber(ARGV[2])
|
|
99
|
+
local limit = tonumber(ARGV[3])
|
|
100
|
+
local windowMs = tonumber(ARGV[4])
|
|
101
|
+
|
|
102
|
+
local raw = redis.call("GET", key)
|
|
103
|
+
local state
|
|
104
|
+
if raw then
|
|
105
|
+
state = cjson.decode(raw)
|
|
106
|
+
else
|
|
107
|
+
local windowStart = math.floor(now / windowMs) * windowMs
|
|
108
|
+
state = {
|
|
109
|
+
currentCount = 0,
|
|
110
|
+
currentWindowStart = windowStart,
|
|
111
|
+
previousCount = 0,
|
|
112
|
+
previousWindowStart = windowStart - windowMs
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
local nextWindowStart = math.floor(now / windowMs) * windowMs
|
|
117
|
+
if state.currentWindowStart ~= nextWindowStart then
|
|
118
|
+
if state.currentWindowStart == nextWindowStart - windowMs then
|
|
119
|
+
state = {
|
|
120
|
+
currentCount = 0,
|
|
121
|
+
currentWindowStart = nextWindowStart,
|
|
122
|
+
previousCount = state.currentCount,
|
|
123
|
+
previousWindowStart = state.currentWindowStart
|
|
124
|
+
}
|
|
125
|
+
else
|
|
126
|
+
state = {
|
|
127
|
+
currentCount = 0,
|
|
128
|
+
currentWindowStart = nextWindowStart,
|
|
129
|
+
previousCount = 0,
|
|
130
|
+
previousWindowStart = nextWindowStart - windowMs
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
local elapsed = now - state.currentWindowStart
|
|
136
|
+
local previousWeight = (windowMs - elapsed) / windowMs
|
|
137
|
+
if previousWeight < 0 then
|
|
138
|
+
previousWeight = 0
|
|
139
|
+
elseif previousWeight > 1 then
|
|
140
|
+
previousWeight = 1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
local effectiveBefore = state.currentCount + (state.previousCount * previousWeight)
|
|
144
|
+
local allowed = 0
|
|
145
|
+
if effectiveBefore + cost <= limit + 0.000000001 then
|
|
146
|
+
allowed = 1
|
|
147
|
+
state.currentCount = state.currentCount + cost
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
local effectiveAfter = state.currentCount + (state.previousCount * previousWeight)
|
|
151
|
+
local remaining = math.max(0, math.floor(limit - effectiveAfter + 0.000000001))
|
|
152
|
+
local used = limit - remaining
|
|
153
|
+
local resetAt = state.currentWindowStart + windowMs
|
|
154
|
+
local retryAfterMs = 0
|
|
155
|
+
|
|
156
|
+
if allowed == 0 then
|
|
157
|
+
if state.currentCount + cost > limit or state.previousCount <= 0 then
|
|
158
|
+
retryAfterMs = math.max(1, resetAt - now)
|
|
159
|
+
else
|
|
160
|
+
local decayBudget = limit - cost - state.currentCount
|
|
161
|
+
if decayBudget < 0 then
|
|
162
|
+
retryAfterMs = math.max(1, resetAt - now)
|
|
163
|
+
else
|
|
164
|
+
local targetWeight = decayBudget / state.previousCount
|
|
165
|
+
if targetWeight < 0 then
|
|
166
|
+
targetWeight = 0
|
|
167
|
+
elseif targetWeight > 1 then
|
|
168
|
+
targetWeight = 1
|
|
169
|
+
end
|
|
170
|
+
local targetElapsed = windowMs - (targetWeight * windowMs)
|
|
171
|
+
retryAfterMs = math.max(1, math.ceil(targetElapsed - elapsed))
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
local ttlMs = windowMs * 2
|
|
177
|
+
redis.call("PSETEX", key, ttlMs, cjson.encode(state))
|
|
178
|
+
return {
|
|
179
|
+
cjson.encode(state),
|
|
180
|
+
tostring(allowed),
|
|
181
|
+
tostring(used),
|
|
182
|
+
tostring(remaining),
|
|
183
|
+
tostring(resetAt),
|
|
184
|
+
tostring(retryAfterMs),
|
|
185
|
+
tostring(ttlMs)
|
|
186
|
+
}
|
|
187
|
+
`;
|
|
188
|
+
return {
|
|
189
|
+
script,
|
|
190
|
+
getArgs(context) {
|
|
191
|
+
return [
|
|
192
|
+
String(context.now),
|
|
193
|
+
String(context.cost),
|
|
194
|
+
String(limit),
|
|
195
|
+
String(windowMs),
|
|
196
|
+
];
|
|
197
|
+
},
|
|
198
|
+
parse(raw) {
|
|
199
|
+
const values = normalizeRawRedisResult(raw, "sliding-window");
|
|
200
|
+
return {
|
|
201
|
+
state: JSON.parse(values[0]),
|
|
202
|
+
allowed: values[1] === "1",
|
|
203
|
+
used: Number(values[2]),
|
|
204
|
+
remaining: Number(values[3]),
|
|
205
|
+
resetAt: Number(values[4]),
|
|
206
|
+
retryAfterMs: Number(values[5]),
|
|
207
|
+
ttlMs: Number(values[6]),
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=sliding-window-strategy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sliding-window-strategy.js","sourceRoot":"","sources":["../../src/strategies/sliding-window-strategy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RateLimitStrategy, RedisStrategyProgram, StrategyExecutionContext, StrategyExecutionSnapshot } from "../types.js";
|
|
2
|
+
export interface TokenBucketConfig {
|
|
3
|
+
capacity: number;
|
|
4
|
+
refillIntervalMs: number;
|
|
5
|
+
refillRate: number;
|
|
6
|
+
}
|
|
7
|
+
export interface TokenBucketState {
|
|
8
|
+
lastRefillAt: number;
|
|
9
|
+
tokens: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Pattern: Strategy
|
|
13
|
+
* Problem: Burst control and rolling windows are separate rate-limit behaviors.
|
|
14
|
+
* Solution: Token bucket is isolated behind the shared strategy contract.
|
|
15
|
+
* Trade-off: Slightly more structure; justified because callers should not care which algorithm is active.
|
|
16
|
+
*/
|
|
17
|
+
export declare class TokenBucketStrategy implements RateLimitStrategy<TokenBucketState> {
|
|
18
|
+
readonly kind = "token-bucket";
|
|
19
|
+
readonly limit: number;
|
|
20
|
+
readonly policy: string;
|
|
21
|
+
readonly redis: RedisStrategyProgram<TokenBucketState>;
|
|
22
|
+
private readonly capacity;
|
|
23
|
+
private readonly refillIntervalMs;
|
|
24
|
+
private readonly refillRate;
|
|
25
|
+
private readonly refillPerMs;
|
|
26
|
+
constructor(config: TokenBucketConfig);
|
|
27
|
+
initialState(now: number): TokenBucketState;
|
|
28
|
+
evaluate(currentState: TokenBucketState | undefined, context: StrategyExecutionContext): StrategyExecutionSnapshot<TokenBucketState>;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=token-bucket-strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-bucket-strategy.d.ts","sourceRoot":"","sources":["../../src/strategies/token-bucket-strategy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,qBAAa,mBACX,YAAW,iBAAiB,CAAC,gBAAgB,CAAC;IAE9C,QAAQ,CAAC,IAAI,kBAAkB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC,gBAAgB,CAAC,CAAC;IACvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAEzB,MAAM,EAAE,iBAAiB;IAqCrC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB;IAO3C,QAAQ,CACN,YAAY,EAAE,gBAAgB,GAAG,SAAS,EAC1C,OAAO,EAAE,wBAAwB,GAChC,yBAAyB,CAAC,gBAAgB,CAAC;CAiC/C"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { RateLimiterConfigurationError } from "../errors.js";
|
|
2
|
+
import { clamp, normalizeRawRedisResult, toWholeNumber } from "../utils/math.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pattern: Strategy
|
|
5
|
+
* Problem: Burst control and rolling windows are separate rate-limit behaviors.
|
|
6
|
+
* Solution: Token bucket is isolated behind the shared strategy contract.
|
|
7
|
+
* Trade-off: Slightly more structure; justified because callers should not care which algorithm is active.
|
|
8
|
+
*/
|
|
9
|
+
export class TokenBucketStrategy {
|
|
10
|
+
kind = "token-bucket";
|
|
11
|
+
limit;
|
|
12
|
+
policy;
|
|
13
|
+
redis;
|
|
14
|
+
capacity;
|
|
15
|
+
refillIntervalMs;
|
|
16
|
+
refillRate;
|
|
17
|
+
refillPerMs;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
if (!Number.isFinite(config.capacity) || config.capacity <= 0) {
|
|
20
|
+
throw new RateLimiterConfigurationError("TokenBucketStrategy requires a positive capacity.");
|
|
21
|
+
}
|
|
22
|
+
if (!Number.isFinite(config.refillIntervalMs) ||
|
|
23
|
+
config.refillIntervalMs <= 0) {
|
|
24
|
+
throw new RateLimiterConfigurationError("TokenBucketStrategy requires a positive refillIntervalMs.");
|
|
25
|
+
}
|
|
26
|
+
if (!Number.isFinite(config.refillRate) || config.refillRate <= 0) {
|
|
27
|
+
throw new RateLimiterConfigurationError("TokenBucketStrategy requires a positive refillRate.");
|
|
28
|
+
}
|
|
29
|
+
this.capacity = config.capacity;
|
|
30
|
+
this.refillIntervalMs = config.refillIntervalMs;
|
|
31
|
+
this.refillRate = config.refillRate;
|
|
32
|
+
this.refillPerMs = this.refillRate / this.refillIntervalMs;
|
|
33
|
+
this.limit = Math.floor(this.capacity);
|
|
34
|
+
this.policy = `${this.limit};w=${Math.ceil(this.refillIntervalMs / 1000)};burst=${this.limit}`;
|
|
35
|
+
this.redis = createTokenBucketRedisProgram(this.capacity, this.refillRate, this.refillIntervalMs);
|
|
36
|
+
}
|
|
37
|
+
initialState(now) {
|
|
38
|
+
return {
|
|
39
|
+
lastRefillAt: now,
|
|
40
|
+
tokens: this.capacity,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
evaluate(currentState, context) {
|
|
44
|
+
const state = currentState
|
|
45
|
+
? { ...currentState }
|
|
46
|
+
: this.initialState(context.now);
|
|
47
|
+
if (context.now > state.lastRefillAt) {
|
|
48
|
+
const refill = (context.now - state.lastRefillAt) * this.refillPerMs;
|
|
49
|
+
state.tokens = clamp(state.tokens + refill, 0, this.capacity);
|
|
50
|
+
state.lastRefillAt = context.now;
|
|
51
|
+
}
|
|
52
|
+
const allowed = state.tokens + 1e-9 >= context.cost;
|
|
53
|
+
if (allowed) {
|
|
54
|
+
state.tokens = clamp(state.tokens - context.cost, 0, this.capacity);
|
|
55
|
+
}
|
|
56
|
+
const retryAfterMs = allowed
|
|
57
|
+
? 0
|
|
58
|
+
: Math.max(1, Math.ceil((context.cost - state.tokens) / this.refillPerMs));
|
|
59
|
+
const timeToFullMs = Math.ceil((this.capacity - state.tokens) / this.refillPerMs);
|
|
60
|
+
return {
|
|
61
|
+
state,
|
|
62
|
+
allowed,
|
|
63
|
+
used: this.limit - toWholeNumber(state.tokens),
|
|
64
|
+
remaining: toWholeNumber(state.tokens),
|
|
65
|
+
resetAt: context.now + Math.max(0, timeToFullMs),
|
|
66
|
+
retryAfterMs,
|
|
67
|
+
ttlMs: Math.max(this.refillIntervalMs, timeToFullMs),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function createTokenBucketRedisProgram(capacity, refillRate, refillIntervalMs) {
|
|
72
|
+
const script = `
|
|
73
|
+
local key = KEYS[1]
|
|
74
|
+
local now = tonumber(ARGV[1])
|
|
75
|
+
local cost = tonumber(ARGV[2])
|
|
76
|
+
local capacity = tonumber(ARGV[3])
|
|
77
|
+
local refillRate = tonumber(ARGV[4])
|
|
78
|
+
local refillIntervalMs = tonumber(ARGV[5])
|
|
79
|
+
local refillPerMs = refillRate / refillIntervalMs
|
|
80
|
+
|
|
81
|
+
local raw = redis.call("GET", key)
|
|
82
|
+
local state
|
|
83
|
+
if raw then
|
|
84
|
+
state = cjson.decode(raw)
|
|
85
|
+
else
|
|
86
|
+
state = {
|
|
87
|
+
lastRefillAt = now,
|
|
88
|
+
tokens = capacity
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if now > state.lastRefillAt then
|
|
93
|
+
local refill = (now - state.lastRefillAt) * refillPerMs
|
|
94
|
+
state.tokens = math.min(capacity, math.max(0, state.tokens + refill))
|
|
95
|
+
state.lastRefillAt = now
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
local allowed = 0
|
|
99
|
+
if state.tokens + 0.000000001 >= cost then
|
|
100
|
+
allowed = 1
|
|
101
|
+
state.tokens = math.min(capacity, math.max(0, state.tokens - cost))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
local remaining = math.max(0, math.floor(state.tokens + 0.000000001))
|
|
105
|
+
local used = math.floor(capacity) - remaining
|
|
106
|
+
local retryAfterMs = 0
|
|
107
|
+
if allowed == 0 then
|
|
108
|
+
retryAfterMs = math.max(1, math.ceil((cost - state.tokens) / refillPerMs))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
local timeToFullMs = math.ceil((capacity - state.tokens) / refillPerMs)
|
|
112
|
+
if timeToFullMs < 0 then
|
|
113
|
+
timeToFullMs = 0
|
|
114
|
+
end
|
|
115
|
+
local ttlMs = math.max(refillIntervalMs, timeToFullMs)
|
|
116
|
+
local resetAt = now + timeToFullMs
|
|
117
|
+
|
|
118
|
+
redis.call("PSETEX", key, ttlMs, cjson.encode(state))
|
|
119
|
+
return {
|
|
120
|
+
cjson.encode(state),
|
|
121
|
+
tostring(allowed),
|
|
122
|
+
tostring(used),
|
|
123
|
+
tostring(remaining),
|
|
124
|
+
tostring(resetAt),
|
|
125
|
+
tostring(retryAfterMs),
|
|
126
|
+
tostring(ttlMs)
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
return {
|
|
130
|
+
script,
|
|
131
|
+
getArgs(context) {
|
|
132
|
+
return [
|
|
133
|
+
String(context.now),
|
|
134
|
+
String(context.cost),
|
|
135
|
+
String(capacity),
|
|
136
|
+
String(refillRate),
|
|
137
|
+
String(refillIntervalMs),
|
|
138
|
+
];
|
|
139
|
+
},
|
|
140
|
+
parse(raw) {
|
|
141
|
+
const values = normalizeRawRedisResult(raw, "token-bucket");
|
|
142
|
+
return {
|
|
143
|
+
state: JSON.parse(values[0]),
|
|
144
|
+
allowed: values[1] === "1",
|
|
145
|
+
used: Number(values[2]),
|
|
146
|
+
remaining: Number(values[3]),
|
|
147
|
+
resetAt: Number(values[4]),
|
|
148
|
+
retryAfterMs: Number(values[5]),
|
|
149
|
+
ttlMs: Number(values[6]),
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=token-bucket-strategy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-bucket-strategy.js","sourceRoot":"","sources":["../../src/strategies/token-bucket-strategy.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface RateLimitCheckOptions {
|
|
2
|
+
cost?: number;
|
|
3
|
+
}
|
|
4
|
+
export interface RateLimitDecision {
|
|
5
|
+
key: string;
|
|
6
|
+
allowed: boolean;
|
|
7
|
+
strategy: string;
|
|
8
|
+
policy: string;
|
|
9
|
+
limit: number;
|
|
10
|
+
used: number;
|
|
11
|
+
remaining: number;
|
|
12
|
+
checkedAt: Date;
|
|
13
|
+
resetAt: Date;
|
|
14
|
+
resetAfterMs: number;
|
|
15
|
+
retryAfterMs: number;
|
|
16
|
+
retryAfterSeconds: number;
|
|
17
|
+
}
|
|
18
|
+
export interface RateLimiterLike {
|
|
19
|
+
check(key: string, options?: RateLimitCheckOptions): Promise<RateLimitDecision>;
|
|
20
|
+
reset(key: string): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export interface StrategyExecutionContext {
|
|
23
|
+
now: number;
|
|
24
|
+
cost: number;
|
|
25
|
+
}
|
|
26
|
+
export interface StrategyExecutionSnapshot<TState> {
|
|
27
|
+
state: TState;
|
|
28
|
+
allowed: boolean;
|
|
29
|
+
used: number;
|
|
30
|
+
remaining: number;
|
|
31
|
+
resetAt: number;
|
|
32
|
+
retryAfterMs: number;
|
|
33
|
+
ttlMs: number;
|
|
34
|
+
}
|
|
35
|
+
export interface RedisStrategyProgram<TState> {
|
|
36
|
+
script: string;
|
|
37
|
+
getArgs(context: StrategyExecutionContext): string[];
|
|
38
|
+
parse(raw: unknown): StrategyExecutionSnapshot<TState>;
|
|
39
|
+
}
|
|
40
|
+
export interface RateLimitStrategy<TState> {
|
|
41
|
+
kind: string;
|
|
42
|
+
limit: number;
|
|
43
|
+
policy: string;
|
|
44
|
+
initialState(now: number): TState;
|
|
45
|
+
evaluate(currentState: TState | undefined, context: StrategyExecutionContext): StrategyExecutionSnapshot<TState>;
|
|
46
|
+
redis?: RedisStrategyProgram<TState>;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAChF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,wBAAwB;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,yBAAyB,CAAC,MAAM;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB,CAAC,MAAM;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,OAAO,EAAE,wBAAwB,GAAG,MAAM,EAAE,CAAC;IACrD,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,yBAAyB,CAAC,MAAM,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,iBAAiB,CAAC,MAAM;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAClC,QAAQ,CACN,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,OAAO,EAAE,wBAAwB,GAChC,yBAAyB,CAAC,MAAM,CAAC,CAAC;IACrC,KAAK,CAAC,EAAE,oBAAoB,CAAC,MAAM,CAAC,CAAC;CACtC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RateLimitDecision } from "../types.js";
|
|
2
|
+
export declare function buildRateLimitHeaders(decision: RateLimitDecision): Record<string, string>;
|
|
3
|
+
export declare function applyHeaders(setHeader: (name: string, value: string) => void, decision: RateLimitDecision): void;
|
|
4
|
+
//# sourceMappingURL=headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/utils/headers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,iBAAiB,GAC1B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAED,wBAAgB,YAAY,CAC1B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,EAChD,QAAQ,EAAE,iBAAiB,GAC1B,IAAI,CAKN"}
|