@stimulcross/rate-limiter 0.0.1 → 0.0.3
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/README.md +20 -0
- package/lib/core/cancellable.d.ts +5 -0
- package/lib/core/cancellable.js +2 -0
- package/lib/core/clock.d.ts +10 -0
- package/lib/core/clock.js +2 -0
- package/{src/core/decision.ts → lib/core/decision.d.ts} +7 -11
- package/lib/core/decision.js +2 -0
- package/lib/core/rate-limit-policy.d.ts +14 -0
- package/lib/core/rate-limit-policy.js +2 -0
- package/lib/core/rate-limiter-status.d.ts +14 -0
- package/lib/core/rate-limiter-status.js +2 -0
- package/lib/core/rate-limiter.d.ts +34 -0
- package/lib/core/rate-limiter.js +2 -0
- package/lib/core/state-storage.d.ts +46 -0
- package/lib/core/state-storage.js +2 -0
- package/lib/enums/rate-limit-error-code.d.ts +26 -0
- package/lib/enums/rate-limit-error-code.js +27 -0
- package/lib/errors/custom.error.d.ts +6 -0
- package/lib/errors/custom.error.js +13 -0
- package/lib/errors/invalid-cost.error.d.ts +16 -0
- package/lib/errors/invalid-cost.error.js +26 -0
- package/lib/errors/rate-limit.error.d.ts +37 -0
- package/lib/errors/rate-limit.error.js +75 -0
- package/lib/errors/rate-limiter-destroyed.error.d.ts +7 -0
- package/lib/errors/rate-limiter-destroyed.error.js +9 -0
- package/{src/index.ts → lib/index.d.ts} +1 -0
- package/lib/index.js +5 -0
- package/lib/interfaces/rate-limiter-options.d.ts +76 -0
- package/lib/interfaces/rate-limiter-options.js +2 -0
- package/lib/interfaces/rate-limiter-queue-options.d.ts +42 -0
- package/lib/interfaces/rate-limiter-queue-options.js +2 -0
- package/lib/interfaces/rate-limiter-run-options.d.ts +52 -0
- package/lib/interfaces/rate-limiter-run-options.js +2 -0
- package/lib/limiters/abstract-rate-limiter.d.ts +44 -0
- package/lib/limiters/abstract-rate-limiter.js +133 -0
- package/lib/limiters/composite.policy.d.ts +15 -0
- package/lib/limiters/composite.policy.js +73 -0
- package/lib/limiters/fixed-window/fixed-window.limiter.d.ts +33 -0
- package/lib/limiters/fixed-window/fixed-window.limiter.js +85 -0
- package/lib/limiters/fixed-window/fixed-window.options.d.ts +27 -0
- package/lib/limiters/fixed-window/fixed-window.options.js +2 -0
- package/lib/limiters/fixed-window/fixed-window.policy.d.ts +19 -0
- package/lib/limiters/fixed-window/fixed-window.policy.js +121 -0
- package/{src/limiters/fixed-window/fixed-window.state.ts → lib/limiters/fixed-window/fixed-window.state.d.ts} +4 -3
- package/lib/limiters/fixed-window/fixed-window.state.js +2 -0
- package/lib/limiters/fixed-window/fixed-window.status.d.ts +39 -0
- package/lib/limiters/fixed-window/fixed-window.status.js +2 -0
- package/{src/limiters/fixed-window/index.ts → lib/limiters/fixed-window/index.d.ts} +1 -0
- package/lib/limiters/fixed-window/index.js +2 -0
- package/lib/limiters/generic-cell/generic-cell.limiter.d.ts +30 -0
- package/lib/limiters/generic-cell/generic-cell.limiter.js +74 -0
- package/lib/limiters/generic-cell/generic-cell.options.d.ts +22 -0
- package/lib/limiters/generic-cell/generic-cell.options.js +2 -0
- package/lib/limiters/generic-cell/generic-cell.policy.d.ts +18 -0
- package/lib/limiters/generic-cell/generic-cell.policy.js +87 -0
- package/{src/limiters/generic-cell/generic-cell.state.ts → lib/limiters/generic-cell/generic-cell.state.d.ts} +2 -1
- package/lib/limiters/generic-cell/generic-cell.state.js +2 -0
- package/lib/limiters/generic-cell/generic-cell.status.d.ts +49 -0
- package/lib/limiters/generic-cell/generic-cell.status.js +2 -0
- package/{src/limiters/generic-cell/index.ts → lib/limiters/generic-cell/index.d.ts} +1 -0
- package/lib/limiters/generic-cell/index.js +2 -0
- package/{src/limiters/http-response-based/http-limit-info.extractor.ts → lib/limiters/http-response-based/http-limit-info.extractor.d.ts} +2 -6
- package/lib/limiters/http-response-based/http-limit-info.extractor.js +2 -0
- package/lib/limiters/http-response-based/http-limit.info.d.ts +39 -0
- package/lib/limiters/http-response-based/http-limit.info.js +2 -0
- package/{src/limiters/http-response-based/http-response-based-limiter.options.ts → lib/limiters/http-response-based/http-response-based-limiter.options.d.ts} +9 -10
- package/lib/limiters/http-response-based/http-response-based-limiter.options.js +2 -0
- package/lib/limiters/http-response-based/http-response-based-limiter.state.d.ts +14 -0
- package/lib/limiters/http-response-based/http-response-based-limiter.state.js +2 -0
- package/lib/limiters/http-response-based/http-response-based-limiter.status.d.ts +70 -0
- package/lib/limiters/http-response-based/http-response-based-limiter.status.js +2 -0
- package/lib/limiters/http-response-based/http-response-based.limiter.d.ts +56 -0
- package/lib/limiters/http-response-based/http-response-based.limiter.js +386 -0
- package/{src/limiters/http-response-based/index.ts → lib/limiters/http-response-based/index.d.ts} +1 -0
- package/lib/limiters/http-response-based/index.js +2 -0
- package/{src/limiters/leaky-bucket/index.ts → lib/limiters/leaky-bucket/index.d.ts} +1 -0
- package/lib/limiters/leaky-bucket/index.js +2 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.limiter.d.ts +30 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.limiter.js +75 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.options.d.ts +22 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.options.js +2 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.policy.d.ts +19 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.policy.js +101 -0
- package/{src/limiters/leaky-bucket/leaky-bucket.state.ts → lib/limiters/leaky-bucket/leaky-bucket.state.d.ts} +3 -2
- package/lib/limiters/leaky-bucket/leaky-bucket.state.js +2 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.status.d.ts +31 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.status.js +2 -0
- package/{src/limiters/sliding-window-counter/index.ts → lib/limiters/sliding-window-counter/index.d.ts} +2 -4
- package/lib/limiters/sliding-window-counter/index.js +2 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.limiter.d.ts +28 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.limiter.js +47 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.options.d.ts +16 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.options.js +2 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.policy.d.ts +18 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.policy.js +128 -0
- package/{src/limiters/sliding-window-counter/sliding-window-counter.state.ts → lib/limiters/sliding-window-counter/sliding-window-counter.state.d.ts} +4 -3
- package/lib/limiters/sliding-window-counter/sliding-window-counter.state.js +2 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.status.d.ts +45 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.status.js +2 -0
- package/{src/limiters/sliding-window-log/index.ts → lib/limiters/sliding-window-log/index.d.ts} +1 -0
- package/lib/limiters/sliding-window-log/index.js +2 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.limiter.d.ts +27 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.limiter.js +44 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.options.d.ts +16 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.options.js +2 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.policy.d.ts +18 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.policy.js +124 -0
- package/{src/limiters/sliding-window-log/sliding-window-log.state.ts → lib/limiters/sliding-window-log/sliding-window-log.state.d.ts} +5 -6
- package/lib/limiters/sliding-window-log/sliding-window-log.state.js +2 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.status.d.ts +39 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.status.js +2 -0
- package/{src/limiters/token-bucket/index.ts → lib/limiters/token-bucket/index.d.ts} +1 -0
- package/lib/limiters/token-bucket/index.js +2 -0
- package/lib/limiters/token-bucket/token-bucket.limiter.d.ts +30 -0
- package/lib/limiters/token-bucket/token-bucket.limiter.js +75 -0
- package/{src/limiters/token-bucket/token-bucket.options.ts → lib/limiters/token-bucket/token-bucket.options.d.ts} +9 -10
- package/lib/limiters/token-bucket/token-bucket.options.js +2 -0
- package/lib/limiters/token-bucket/token-bucket.policy.d.ts +19 -0
- package/lib/limiters/token-bucket/token-bucket.policy.js +116 -0
- package/{src/limiters/token-bucket/token-bucket.state.ts → lib/limiters/token-bucket/token-bucket.state.d.ts} +4 -3
- package/lib/limiters/token-bucket/token-bucket.state.js +2 -0
- package/lib/limiters/token-bucket/token-bucket.status.d.ts +31 -0
- package/lib/limiters/token-bucket/token-bucket.status.js +2 -0
- package/lib/runtime/default-clock.d.ts +4 -0
- package/lib/runtime/default-clock.js +7 -0
- package/lib/runtime/execution-tickets.d.ts +12 -0
- package/lib/runtime/execution-tickets.js +27 -0
- package/lib/runtime/in-memory-state-store.d.ts +19 -0
- package/lib/runtime/in-memory-state-store.js +97 -0
- package/lib/runtime/rate-limiter.executor.d.ts +47 -0
- package/lib/runtime/rate-limiter.executor.js +196 -0
- package/lib/runtime/semaphore.d.ts +9 -0
- package/lib/runtime/semaphore.js +28 -0
- package/lib/runtime/task.d.ts +41 -0
- package/lib/runtime/task.js +101 -0
- package/{src/types/limit-behavior.ts → lib/types/limit-behavior.d.ts} +1 -0
- package/lib/types/limit-behavior.js +2 -0
- package/lib/utils/generate-random-string.d.ts +3 -0
- package/lib/utils/generate-random-string.js +13 -0
- package/lib/utils/promise-with-resolvers.d.ts +9 -0
- package/lib/utils/promise-with-resolvers.js +15 -0
- package/lib/utils/sanitize-error.d.ts +3 -0
- package/lib/utils/sanitize-error.js +5 -0
- package/lib/utils/sanitize-priority.d.ts +4 -0
- package/lib/utils/sanitize-priority.js +18 -0
- package/lib/utils/validate-cost.d.ts +3 -0
- package/lib/utils/validate-cost.js +14 -0
- package/package.json +13 -2
- package/.editorconfig +0 -21
- package/.github/workflows/node.yml +0 -87
- package/.husky/commit-msg +0 -1
- package/.husky/pre-commit +0 -1
- package/.megaignore +0 -8
- package/.prettierignore +0 -3
- package/commitlint.config.js +0 -8
- package/eslint.config.js +0 -65
- package/lint-staged.config.js +0 -4
- package/prettier.config.cjs +0 -1
- package/src/core/cancellable.ts +0 -4
- package/src/core/clock.ts +0 -9
- package/src/core/rate-limit-policy.ts +0 -15
- package/src/core/rate-limiter-status.ts +0 -14
- package/src/core/rate-limiter.ts +0 -37
- package/src/core/state-storage.ts +0 -51
- package/src/enums/rate-limit-error-code.ts +0 -29
- package/src/errors/custom.error.ts +0 -14
- package/src/errors/invalid-cost.error.ts +0 -33
- package/src/errors/rate-limit.error.ts +0 -91
- package/src/errors/rate-limiter-destroyed.error.ts +0 -8
- package/src/interfaces/rate-limiter-options.ts +0 -84
- package/src/interfaces/rate-limiter-queue-options.ts +0 -45
- package/src/interfaces/rate-limiter-run-options.ts +0 -58
- package/src/limiters/abstract-rate-limiter.ts +0 -206
- package/src/limiters/composite.policy.ts +0 -102
- package/src/limiters/fixed-window/fixed-window.limiter.ts +0 -121
- package/src/limiters/fixed-window/fixed-window.options.ts +0 -29
- package/src/limiters/fixed-window/fixed-window.policy.ts +0 -159
- package/src/limiters/fixed-window/fixed-window.status.ts +0 -46
- package/src/limiters/generic-cell/generic-cell.limiter.ts +0 -108
- package/src/limiters/generic-cell/generic-cell.options.ts +0 -23
- package/src/limiters/generic-cell/generic-cell.policy.ts +0 -115
- package/src/limiters/generic-cell/generic-cell.status.ts +0 -54
- package/src/limiters/http-response-based/http-limit.info.ts +0 -41
- package/src/limiters/http-response-based/http-response-based-limiter.state.ts +0 -13
- package/src/limiters/http-response-based/http-response-based-limiter.status.ts +0 -74
- package/src/limiters/http-response-based/http-response-based.limiter.ts +0 -512
- package/src/limiters/leaky-bucket/leaky-bucket.limiter.ts +0 -105
- package/src/limiters/leaky-bucket/leaky-bucket.options.ts +0 -23
- package/src/limiters/leaky-bucket/leaky-bucket.policy.ts +0 -134
- package/src/limiters/leaky-bucket/leaky-bucket.status.ts +0 -36
- package/src/limiters/sliding-window-counter/sliding-window-counter.limiter.ts +0 -76
- package/src/limiters/sliding-window-counter/sliding-window-counter.options.ts +0 -20
- package/src/limiters/sliding-window-counter/sliding-window-counter.policy.ts +0 -167
- package/src/limiters/sliding-window-counter/sliding-window-counter.status.ts +0 -53
- package/src/limiters/sliding-window-log/sliding-window-log.limiter.ts +0 -65
- package/src/limiters/sliding-window-log/sliding-window-log.options.ts +0 -20
- package/src/limiters/sliding-window-log/sliding-window-log.policy.ts +0 -166
- package/src/limiters/sliding-window-log/sliding-window-log.status.ts +0 -44
- package/src/limiters/token-bucket/token-bucket.limiter.ts +0 -110
- package/src/limiters/token-bucket/token-bucket.policy.ts +0 -155
- package/src/limiters/token-bucket/token-bucket.status.ts +0 -36
- package/src/runtime/default-clock.ts +0 -8
- package/src/runtime/execution-tickets.ts +0 -34
- package/src/runtime/in-memory-state-store.ts +0 -135
- package/src/runtime/rate-limiter.executor.ts +0 -286
- package/src/runtime/semaphore.ts +0 -31
- package/src/runtime/task.ts +0 -141
- package/src/utils/generate-random-string.ts +0 -16
- package/src/utils/promise-with-resolvers.ts +0 -23
- package/src/utils/sanitize-error.ts +0 -4
- package/src/utils/sanitize-priority.ts +0 -22
- package/src/utils/validate-cost.ts +0 -16
- package/tests/integration/limiters/fixed-window.limiter.spec.ts +0 -371
- package/tests/integration/limiters/generic-cell.limiter.spec.ts +0 -361
- package/tests/integration/limiters/http-response-based.limiter.spec.ts +0 -833
- package/tests/integration/limiters/leaky-bucket.spec.ts +0 -357
- package/tests/integration/limiters/sliding-window-counter.limiter.spec.ts +0 -175
- package/tests/integration/limiters/sliding-window-log.spec.ts +0 -185
- package/tests/integration/limiters/token-bucket.limiter.spec.ts +0 -363
- package/tests/tsconfig.json +0 -4
- package/tests/unit/policies/composite.policy.spec.ts +0 -244
- package/tests/unit/policies/fixed-window.policy.spec.ts +0 -260
- package/tests/unit/policies/generic-cell.policy.spec.ts +0 -178
- package/tests/unit/policies/leaky-bucket.policy.spec.ts +0 -215
- package/tests/unit/policies/sliding-window-counter.policy.spec.ts +0 -209
- package/tests/unit/policies/sliding-window-log.policy.spec.ts +0 -285
- package/tests/unit/policies/token-bucket.policy.spec.ts +0 -371
- package/tests/unit/runtime/execution-tickets.spec.ts +0 -121
- package/tests/unit/runtime/in-memory-state-store.spec.ts +0 -238
- package/tests/unit/runtime/rate-limiter.executor.spec.ts +0 -353
- package/tests/unit/runtime/semaphore.spec.ts +0 -98
- package/tests/unit/runtime/task.spec.ts +0 -182
- package/tests/unit/utils/generate-random-string.spec.ts +0 -51
- package/tests/unit/utils/promise-with-resolvers.spec.ts +0 -57
- package/tests/unit/utils/sanitize-priority.spec.ts +0 -46
- package/tests/unit/utils/validate-cost.spec.ts +0 -48
- package/tsconfig.json +0 -14
- package/vitest.config.js +0 -22
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { LogLevel } from '@stimulcross/logger';
|
|
2
|
-
import { GenericCellPolicy } from './generic-cell.policy.js';
|
|
3
|
-
import { AbstractRateLimiter, type ExecutionContext } from '../abstract-rate-limiter.js';
|
|
4
|
-
import { type GenericCellOptions } from './generic-cell.options.js';
|
|
5
|
-
import { type GenericCellState } from './generic-cell.state.js';
|
|
6
|
-
import { type GenericCellStatus } from './generic-cell.status.js';
|
|
7
|
-
import { type Decision } from '../../core/decision.js';
|
|
8
|
-
import { RateLimitErrorCode } from '../../enums/rate-limit-error-code.js';
|
|
9
|
-
import { RateLimitError } from '../../errors/rate-limit.error.js';
|
|
10
|
-
import { type LimitBehavior } from '../../types/limit-behavior.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Generic Cell (GCRA) rate limiter.
|
|
14
|
-
*
|
|
15
|
-
* Designed primarily for client-side use to respect third-party limits or protect resources.
|
|
16
|
-
* While this can be used as a server-side limiter with custom distributed storage
|
|
17
|
-
* (e.g., Redis), it is best-effort and not recommended due to high network round-trip latency.
|
|
18
|
-
*
|
|
19
|
-
* Key features:
|
|
20
|
-
* - **Queueing & overflow** - optionally enqueues excess requests up to a maximum allowed overflow capacity
|
|
21
|
-
* - **Concurrency** - limits how many requests can be executed simultaneously
|
|
22
|
-
* - **Priority** - supports task priorities (with fairness and custom policy) to execute critical requests first
|
|
23
|
-
* - **Cancellation** - supports `AbortSignal` to safely remove pending requests from the queue
|
|
24
|
-
* - **Expiration** - automatically drops queued requests that wait longer than the allowed `maxWaitMs`
|
|
25
|
-
* - **Auto-rollback** - reverts spent quota if an enqueued task is canceled or expired
|
|
26
|
-
*/
|
|
27
|
-
export class GenericCellLimiter extends AbstractRateLimiter<GenericCellState, GenericCellStatus> {
|
|
28
|
-
private readonly _defaultLimitBehaviour: LimitBehavior;
|
|
29
|
-
private readonly _defaultMaxWaitMs: number | undefined;
|
|
30
|
-
|
|
31
|
-
protected override readonly _policy: GenericCellPolicy;
|
|
32
|
-
|
|
33
|
-
constructor(options: GenericCellOptions) {
|
|
34
|
-
super(options);
|
|
35
|
-
|
|
36
|
-
this._defaultLimitBehaviour = options.limitBehavior ?? 'reject';
|
|
37
|
-
|
|
38
|
-
if (options.queue?.maxWaitMs) {
|
|
39
|
-
this._defaultMaxWaitMs = options.queue.maxWaitMs;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
this._policy = new GenericCellPolicy(options.intervalMs, options.burst);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
protected override async _runInternal<T>(fn: () => T | Promise<T>, ctx: ExecutionContext): Promise<T> {
|
|
46
|
-
const now = this._clock.now();
|
|
47
|
-
const baseTtlMs: number = Math.ceil(this._policy.burst * this._policy.intervalMs);
|
|
48
|
-
|
|
49
|
-
let runAt: number;
|
|
50
|
-
let storeTtlMs: number;
|
|
51
|
-
|
|
52
|
-
await this._store.acquireLock?.(ctx.key);
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
const state = (await this._store.get(ctx.key)) ?? this._policy.getInitialState();
|
|
56
|
-
const finalLimitBehavior = ctx.limitBehavior ?? this._defaultLimitBehaviour;
|
|
57
|
-
|
|
58
|
-
const { decision, nextState } = this._policy.evaluate(
|
|
59
|
-
state,
|
|
60
|
-
now,
|
|
61
|
-
ctx.cost,
|
|
62
|
-
finalLimitBehavior === 'enqueue',
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
if (decision.kind === 'deny') {
|
|
66
|
-
this._logger.debug(`[DENY] [id: ${ctx.id}, key: ${ctx.key}] - Retry: +${decision.retryAt - now}ms`);
|
|
67
|
-
throw new RateLimitError(RateLimitErrorCode.LimitExceeded, decision.retryAt);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
runAt = decision.kind === 'delay' ? decision.runAt : now;
|
|
71
|
-
storeTtlMs = Math.max(baseTtlMs, runAt - now + baseTtlMs);
|
|
72
|
-
|
|
73
|
-
await this._store.set(ctx.key, nextState, storeTtlMs);
|
|
74
|
-
|
|
75
|
-
this._printSuccessDebug(decision, nextState, now, ctx);
|
|
76
|
-
} finally {
|
|
77
|
-
await this._store.releaseLock?.(ctx.key);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const finalMaxWaitMs = ctx.maxWaitMs ?? this._defaultMaxWaitMs;
|
|
81
|
-
const expiresAt = finalMaxWaitMs ? now + finalMaxWaitMs : undefined;
|
|
82
|
-
|
|
83
|
-
return await this._execute<T>(fn, runAt, storeTtlMs, ctx, expiresAt);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
protected override _getDebugStateString(state: GenericCellState): string {
|
|
87
|
-
return String(state.tat);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
private _printSuccessDebug(
|
|
91
|
-
decision: Decision,
|
|
92
|
-
nextState: GenericCellState,
|
|
93
|
-
now: number,
|
|
94
|
-
ctx: ExecutionContext,
|
|
95
|
-
): void {
|
|
96
|
-
if (this._logger.minLevel < LogLevel.DEBUG) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (decision.kind === 'delay') {
|
|
101
|
-
this._logger.debug(
|
|
102
|
-
`[DELAY] [id: ${ctx.id}, key: ${ctx.key}] +${decision.runAt - now}ms - tat: ${nextState.tat}`,
|
|
103
|
-
);
|
|
104
|
-
} else {
|
|
105
|
-
this._logger.debug(`[ALLOW] [id: ${ctx.id}, key: ${ctx.key}] - tat: ${nextState.tat}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { type GenericCellState } from './generic-cell.state.js';
|
|
2
|
-
import { type RateLimiterOptions } from '../../interfaces/rate-limiter-options.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Options for the Generic Cell Rate Algorithm (GCRA) limiter.
|
|
6
|
-
*/
|
|
7
|
-
export interface GenericCellOptions extends RateLimiterOptions<GenericCellState> {
|
|
8
|
-
/**
|
|
9
|
-
* The minimum interval between requests (in milliseconds).
|
|
10
|
-
*
|
|
11
|
-
* This defines the emission interval.
|
|
12
|
-
* For example, if you want to allow 10 requests per second, set this to 100ms (1000ms / 10).
|
|
13
|
-
*/
|
|
14
|
-
intervalMs: number;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* The maximum burst size.
|
|
18
|
-
*
|
|
19
|
-
* This defines how many requests can be made immediately in quick succession before rate limiting kicks in.
|
|
20
|
-
* The burst capacity allows temporary spikes in traffic.
|
|
21
|
-
*/
|
|
22
|
-
burst: number;
|
|
23
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { type GenericCellState } from './generic-cell.state.js';
|
|
2
|
-
import { type GenericCellStatus } from './generic-cell.status.js';
|
|
3
|
-
import { type RateLimitPolicy, type RateLimitPolicyResult } from '../../core/rate-limit-policy.js';
|
|
4
|
-
import { validateCost } from '../../utils/validate-cost.js';
|
|
5
|
-
|
|
6
|
-
/** @internal */
|
|
7
|
-
export class GenericCellPolicy implements RateLimitPolicy<GenericCellState, GenericCellStatus> {
|
|
8
|
-
constructor(
|
|
9
|
-
private readonly _intervalMs: number,
|
|
10
|
-
private readonly _burst: number,
|
|
11
|
-
private readonly _maxDelayMs: number = Number.POSITIVE_INFINITY,
|
|
12
|
-
) {
|
|
13
|
-
if (!Number.isFinite(_intervalMs) || !Number.isSafeInteger(_intervalMs) || _intervalMs <= 0) {
|
|
14
|
-
throw new Error(`Invalid intervalMs: ${_intervalMs}. Must be a positive integer.`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (!Number.isFinite(_burst) || !Number.isSafeInteger(_burst) || _burst <= 0) {
|
|
18
|
-
throw new Error(`Invalid burst: ${_burst}. Must be a positive integer.`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (_maxDelayMs < 0 || (!Number.isSafeInteger(_maxDelayMs) && _maxDelayMs !== Number.POSITIVE_INFINITY)) {
|
|
22
|
-
throw new Error(`Invalid maxDelayMs: ${_maxDelayMs}. Must be a non-negative integer or Infinity.`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
public get burst(): number {
|
|
27
|
-
return this._burst;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
public get intervalMs(): number {
|
|
31
|
-
return this._intervalMs;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
public getInitialState(): GenericCellState {
|
|
35
|
-
return { tat: 0 };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
public getStatus(state: GenericCellState, now: number): GenericCellStatus {
|
|
39
|
-
const tat = Math.max(state.tat, now);
|
|
40
|
-
const burstOffset = this._burst * this._intervalMs;
|
|
41
|
-
|
|
42
|
-
const availableTimeDebt = burstOffset + now - tat;
|
|
43
|
-
const remaining = Math.max(0, Math.floor(availableTimeDebt / this._intervalMs));
|
|
44
|
-
|
|
45
|
-
const nextAvailableAt = Math.max(now, tat + this._intervalMs - burstOffset);
|
|
46
|
-
|
|
47
|
-
const resetAt = tat;
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
intervalMs: this._intervalMs,
|
|
51
|
-
burst: this._burst,
|
|
52
|
-
tat,
|
|
53
|
-
remaining,
|
|
54
|
-
nextAvailableAt,
|
|
55
|
-
resetAt,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
public evaluate(
|
|
60
|
-
state: GenericCellState,
|
|
61
|
-
now: number,
|
|
62
|
-
cost: number,
|
|
63
|
-
shouldReserve?: boolean,
|
|
64
|
-
): RateLimitPolicyResult<GenericCellState> {
|
|
65
|
-
const absoluteMax = shouldReserve ? this._burst + Math.floor(this._maxDelayMs / this._intervalMs) : this._burst;
|
|
66
|
-
|
|
67
|
-
validateCost(cost, absoluteMax);
|
|
68
|
-
|
|
69
|
-
const tat = Math.max(state.tat, now);
|
|
70
|
-
const costInterval = cost * this._intervalMs;
|
|
71
|
-
const burstOffset = this._burst * this._intervalMs;
|
|
72
|
-
|
|
73
|
-
const newTat = tat + costInterval;
|
|
74
|
-
|
|
75
|
-
const minNow = newTat - burstOffset;
|
|
76
|
-
|
|
77
|
-
if (now >= minNow) {
|
|
78
|
-
return {
|
|
79
|
-
decision: { kind: 'allow' },
|
|
80
|
-
nextState: { tat: newTat },
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (shouldReserve) {
|
|
85
|
-
const waitMs = minNow - now;
|
|
86
|
-
|
|
87
|
-
if (waitMs > this._maxDelayMs) {
|
|
88
|
-
const retryAt = minNow - this._maxDelayMs;
|
|
89
|
-
return this._deny(state.tat, retryAt);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const runAt = minNow;
|
|
93
|
-
return {
|
|
94
|
-
decision: { kind: 'delay', runAt },
|
|
95
|
-
nextState: { tat: newTat },
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return this._deny(state.tat, minNow);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
public revert(state: GenericCellState, cost: number): GenericCellState {
|
|
103
|
-
const costInterval = cost * this._intervalMs;
|
|
104
|
-
return {
|
|
105
|
-
tat: Math.max(0, state.tat - costInterval),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private _deny(tat: number, retryAt: number): RateLimitPolicyResult<GenericCellState> {
|
|
110
|
-
return {
|
|
111
|
-
decision: { kind: 'deny', retryAt },
|
|
112
|
-
nextState: { tat },
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { type RateLimiterStatus } from '../../core/rate-limiter-status.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The status of the Generic Cell rate limiter.
|
|
5
|
-
*/
|
|
6
|
-
export interface GenericCellStatus extends RateLimiterStatus {
|
|
7
|
-
/**
|
|
8
|
-
* The minimum interval between tokens (in milliseconds).
|
|
9
|
-
*
|
|
10
|
-
* It represents the emission interval - the time period between
|
|
11
|
-
* successive token emissions at the steady-state rate.
|
|
12
|
-
*/
|
|
13
|
-
readonly intervalMs: number;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* The maximum burst size (maximum number of tokens that can be accumulated).
|
|
17
|
-
*
|
|
18
|
-
* It represents the burst capacity - the maximum number of requests
|
|
19
|
-
* that can be made instantaneously when the bucket is full.
|
|
20
|
-
*/
|
|
21
|
-
readonly burst: number;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Theoretical Arrival Time (TAT) - the virtual time when the bucket will be empty.
|
|
25
|
-
*
|
|
26
|
-
* TAT represents the earliest time at which a future request could be allowed.
|
|
27
|
-
*
|
|
28
|
-
* Value is in milliseconds.
|
|
29
|
-
*/
|
|
30
|
-
readonly tat: number;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The number of tokens that can be used immediately.
|
|
34
|
-
*
|
|
35
|
-
* Calculated as the time allowance available between now and TAT, divided by
|
|
36
|
-
* the emission interval.
|
|
37
|
-
*/
|
|
38
|
-
readonly remaining: number;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* The timestamp (in milliseconds) when the next token will be available for use.
|
|
42
|
-
*
|
|
43
|
-
* This is the earliest time when a request can be accepted if no tokens are currently available.
|
|
44
|
-
*/
|
|
45
|
-
readonly nextAvailableAt: number;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The timestamp when all tokens will be available for use again.
|
|
49
|
-
*
|
|
50
|
-
* It is when the TAT returns to the current time, meaning the bucket has fully recovered to its
|
|
51
|
-
* maximum burst capacity.
|
|
52
|
-
*/
|
|
53
|
-
readonly resetAt: number;
|
|
54
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rate limit information extracted from the HTTP response headers.
|
|
3
|
-
*/
|
|
4
|
-
export interface HttpLimitInfo {
|
|
5
|
-
/**
|
|
6
|
-
* The maximum number of requests allowed within the time window.
|
|
7
|
-
*
|
|
8
|
-
* It usually corresponds to `RateLimit-Limit` or `X-RateLimit-Limit` headers.
|
|
9
|
-
*/
|
|
10
|
-
limit: number;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The number of requests remaining in the current time window.
|
|
14
|
-
*
|
|
15
|
-
* It usually corresponds to `RateLimit-Remaining` or `X-RateLimit-Remaining` headers.
|
|
16
|
-
*/
|
|
17
|
-
remaining: number;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The timestamp (in milliseconds) when the current time window resets.
|
|
21
|
-
*
|
|
22
|
-
* It usually corresponds to `RateLimit-Reset`, `X-RateLimit-Reset`, or `Retry-After` headers.
|
|
23
|
-
*
|
|
24
|
-
* **WARNING:** these headers can be either delta (usually in seconds) or UNIX epoche
|
|
25
|
-
* timestamp (usually in seconds) depending on the target API specs.
|
|
26
|
-
*
|
|
27
|
-
*The library expects a UNIX epoche timestamp in milliseconds. Refer to the API docs to
|
|
28
|
-
* determine the header format and properly convert it to UNIX epoche timestamp in milliseconds.
|
|
29
|
-
*
|
|
30
|
-
* For example,
|
|
31
|
-
* - delta in seconds: `Date.now() + delta * 1000`
|
|
32
|
-
* - UNIX timestamp in seconds: `timestamp * 1000`
|
|
33
|
-
*
|
|
34
|
-
*/
|
|
35
|
-
resetAt?: number | null;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* HTTP status code of the response.
|
|
39
|
-
*/
|
|
40
|
-
statusCode: number;
|
|
41
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The state of the HTTP Response Based rate limiter.
|
|
3
|
-
*
|
|
4
|
-
* When using a distributed state store, make sure it properly serializes and deserializes the state.
|
|
5
|
-
*/
|
|
6
|
-
export interface HttpResponseBasedLimiterState {
|
|
7
|
-
isProbing: boolean;
|
|
8
|
-
isUnlimited: boolean;
|
|
9
|
-
lastKnownLimit: number | null;
|
|
10
|
-
lastKnownRemaining: number | null;
|
|
11
|
-
lastKnownResetAt: number | null;
|
|
12
|
-
lastSyncedAt: number | null;
|
|
13
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The status of the HTTP Response Based rate limiter.
|
|
3
|
-
*
|
|
4
|
-
* This interface represents rate limit information extracted from HTTP response headers,
|
|
5
|
-
* such as RateLimit headers (RFC 6585) or custom headers provided by APIs (X-RateLimit-*).
|
|
6
|
-
*
|
|
7
|
-
* @remarks
|
|
8
|
-
* The status is updated after each API response and reflects the server-side rate limit state.
|
|
9
|
-
* Can be useful to check how many requests remain before hitting the rate limit.
|
|
10
|
-
*/
|
|
11
|
-
export interface HttpResponseBasedLimiterStatus {
|
|
12
|
-
/**
|
|
13
|
-
* Indicates whether the rate limiter is currently probing for the server's limit state.
|
|
14
|
-
*
|
|
15
|
-
* This is `true` when the rate limiter is waiting for the first response from the server
|
|
16
|
-
* to extract rate limit headers.
|
|
17
|
-
*/
|
|
18
|
-
isProbing: boolean;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Indicates whether the rate limiter is in unlimited mode.
|
|
22
|
-
*
|
|
23
|
-
* This is `true` if a server did not send any rate limit headers.
|
|
24
|
-
*
|
|
25
|
-
* If this is `true`, the `lastKnownLimit`, `lastKnownRemaining`, and `lastKnownResetAt` values are `null`.
|
|
26
|
-
*/
|
|
27
|
-
isUnlimited: boolean;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* The number of requests remaining in the current rate limit window.
|
|
31
|
-
*
|
|
32
|
-
* @remarks
|
|
33
|
-
* This value is extracted from response headers such as `X-RateLimit-Remaining`
|
|
34
|
-
* or `RateLimit-Remaining`. It decrements with each request and resets when
|
|
35
|
-
* the time window expires.
|
|
36
|
-
*
|
|
37
|
-
* When this reaches 0, later requests may be delayed or rejected until the reset time.
|
|
38
|
-
*/
|
|
39
|
-
lastKnownRemaining: number | null;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The maximum number of requests allowed in the current rate limit window.
|
|
43
|
-
*
|
|
44
|
-
* @remarks
|
|
45
|
-
* This value is extracted from response headers such as `X-RateLimit-Limit`
|
|
46
|
-
* or `RateLimit-Limit`. It represents the total quota allocated by the API
|
|
47
|
-
* and typically remains constant across requests within the same window.
|
|
48
|
-
*/
|
|
49
|
-
lastKnownLimit: number | null;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* The timestamp (in milliseconds since Unix epoch) when the rate limit window resets.
|
|
53
|
-
*
|
|
54
|
-
* `null` if the reset time is not known or not applicable.
|
|
55
|
-
*
|
|
56
|
-
* @remarks
|
|
57
|
-
* This value is extracted from response headers such as `X-RateLimit-Reset`
|
|
58
|
-
* or `RateLimit-Reset`. The exact semantics depend on the API specification:
|
|
59
|
-
*
|
|
60
|
-
* - **Full window reset**: When the entire rate limit quota is restored
|
|
61
|
-
* - **Next request availability**: When the next single request slot becomes available
|
|
62
|
-
* - **Sliding window**: When the oldest request in the window expires
|
|
63
|
-
*
|
|
64
|
-
* Always refer to your API's documentation to understand the reset behavior.
|
|
65
|
-
*/
|
|
66
|
-
lastKnownResetAt: number | null;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* The timestamp (in milliseconds since Unix epoch) when the status was last synced with the server.
|
|
70
|
-
*
|
|
71
|
-
* `null` if the status has never been synced.
|
|
72
|
-
*/
|
|
73
|
-
lastSyncedAt: number | null;
|
|
74
|
-
}
|