@stimulcross/rate-limiter 0.0.1 → 0.0.2

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.
Files changed (183) hide show
  1. package/README.md +20 -0
  2. package/lib/core/cancellable.js +1 -0
  3. package/lib/core/clock.js +1 -0
  4. package/lib/core/decision.js +1 -0
  5. package/lib/core/rate-limit-policy.js +1 -0
  6. package/lib/core/rate-limiter-status.js +1 -0
  7. package/lib/core/rate-limiter.js +1 -0
  8. package/lib/core/state-storage.js +1 -0
  9. package/lib/enums/rate-limit-error-code.js +26 -0
  10. package/lib/errors/custom.error.js +12 -0
  11. package/lib/errors/invalid-cost.error.js +25 -0
  12. package/lib/errors/rate-limit.error.js +74 -0
  13. package/{src/errors/rate-limiter-destroyed.error.ts → lib/errors/rate-limiter-destroyed.error.js} +3 -3
  14. package/lib/index.js +4 -0
  15. package/lib/interfaces/rate-limiter-options.js +1 -0
  16. package/lib/interfaces/rate-limiter-queue-options.js +1 -0
  17. package/lib/interfaces/rate-limiter-run-options.js +1 -0
  18. package/lib/limiters/abstract-rate-limiter.js +132 -0
  19. package/lib/limiters/composite.policy.js +72 -0
  20. package/lib/limiters/fixed-window/fixed-window.limiter.js +84 -0
  21. package/lib/limiters/fixed-window/fixed-window.options.js +1 -0
  22. package/lib/limiters/fixed-window/fixed-window.policy.js +120 -0
  23. package/lib/limiters/fixed-window/fixed-window.state.js +1 -0
  24. package/lib/limiters/fixed-window/fixed-window.status.js +1 -0
  25. package/lib/limiters/fixed-window/index.js +1 -0
  26. package/lib/limiters/generic-cell/generic-cell.limiter.js +73 -0
  27. package/lib/limiters/generic-cell/generic-cell.options.js +1 -0
  28. package/lib/limiters/generic-cell/generic-cell.policy.js +86 -0
  29. package/lib/limiters/generic-cell/generic-cell.state.js +1 -0
  30. package/lib/limiters/generic-cell/generic-cell.status.js +1 -0
  31. package/lib/limiters/generic-cell/index.js +1 -0
  32. package/lib/limiters/http-response-based/http-limit-info.extractor.js +1 -0
  33. package/lib/limiters/http-response-based/http-limit.info.js +1 -0
  34. package/lib/limiters/http-response-based/http-response-based-limiter.options.js +1 -0
  35. package/lib/limiters/http-response-based/http-response-based-limiter.state.js +1 -0
  36. package/lib/limiters/http-response-based/http-response-based-limiter.status.js +1 -0
  37. package/lib/limiters/http-response-based/http-response-based.limiter.js +379 -0
  38. package/lib/limiters/http-response-based/index.js +1 -0
  39. package/lib/limiters/leaky-bucket/index.js +1 -0
  40. package/lib/limiters/leaky-bucket/leaky-bucket.limiter.js +74 -0
  41. package/lib/limiters/leaky-bucket/leaky-bucket.options.js +1 -0
  42. package/lib/limiters/leaky-bucket/leaky-bucket.policy.js +100 -0
  43. package/lib/limiters/leaky-bucket/leaky-bucket.state.js +1 -0
  44. package/lib/limiters/leaky-bucket/leaky-bucket.status.js +1 -0
  45. package/lib/limiters/sliding-window-counter/index.js +1 -0
  46. package/lib/limiters/sliding-window-counter/sliding-window-counter.limiter.js +46 -0
  47. package/lib/limiters/sliding-window-counter/sliding-window-counter.options.js +1 -0
  48. package/lib/limiters/sliding-window-counter/sliding-window-counter.policy.js +127 -0
  49. package/lib/limiters/sliding-window-counter/sliding-window-counter.state.js +1 -0
  50. package/lib/limiters/sliding-window-counter/sliding-window-counter.status.js +1 -0
  51. package/lib/limiters/sliding-window-log/index.js +1 -0
  52. package/lib/limiters/sliding-window-log/sliding-window-log.limiter.js +43 -0
  53. package/lib/limiters/sliding-window-log/sliding-window-log.options.js +1 -0
  54. package/lib/limiters/sliding-window-log/sliding-window-log.policy.js +123 -0
  55. package/lib/limiters/sliding-window-log/sliding-window-log.state.js +1 -0
  56. package/lib/limiters/sliding-window-log/sliding-window-log.status.js +1 -0
  57. package/lib/limiters/token-bucket/index.js +1 -0
  58. package/lib/limiters/token-bucket/token-bucket.limiter.js +74 -0
  59. package/lib/limiters/token-bucket/token-bucket.options.js +1 -0
  60. package/lib/limiters/token-bucket/token-bucket.policy.js +115 -0
  61. package/lib/limiters/token-bucket/token-bucket.state.js +1 -0
  62. package/lib/limiters/token-bucket/token-bucket.status.js +1 -0
  63. package/lib/runtime/default-clock.js +6 -0
  64. package/lib/runtime/execution-tickets.js +26 -0
  65. package/lib/runtime/in-memory-state-store.js +96 -0
  66. package/lib/runtime/rate-limiter.executor.js +195 -0
  67. package/lib/runtime/semaphore.js +27 -0
  68. package/lib/runtime/task.js +100 -0
  69. package/lib/types/limit-behavior.js +1 -0
  70. package/lib/utils/generate-random-string.js +12 -0
  71. package/lib/utils/promise-with-resolvers.js +14 -0
  72. package/lib/utils/sanitize-error.js +4 -0
  73. package/lib/utils/sanitize-priority.js +17 -0
  74. package/lib/utils/validate-cost.js +13 -0
  75. package/package.json +12 -2
  76. package/.editorconfig +0 -21
  77. package/.github/workflows/node.yml +0 -87
  78. package/.husky/commit-msg +0 -1
  79. package/.husky/pre-commit +0 -1
  80. package/.megaignore +0 -8
  81. package/.prettierignore +0 -3
  82. package/commitlint.config.js +0 -8
  83. package/eslint.config.js +0 -65
  84. package/lint-staged.config.js +0 -4
  85. package/prettier.config.cjs +0 -1
  86. package/src/core/cancellable.ts +0 -4
  87. package/src/core/clock.ts +0 -9
  88. package/src/core/decision.ts +0 -27
  89. package/src/core/rate-limit-policy.ts +0 -15
  90. package/src/core/rate-limiter-status.ts +0 -14
  91. package/src/core/rate-limiter.ts +0 -37
  92. package/src/core/state-storage.ts +0 -51
  93. package/src/enums/rate-limit-error-code.ts +0 -29
  94. package/src/errors/custom.error.ts +0 -14
  95. package/src/errors/invalid-cost.error.ts +0 -33
  96. package/src/errors/rate-limit.error.ts +0 -91
  97. package/src/index.ts +0 -11
  98. package/src/interfaces/rate-limiter-options.ts +0 -84
  99. package/src/interfaces/rate-limiter-queue-options.ts +0 -45
  100. package/src/interfaces/rate-limiter-run-options.ts +0 -58
  101. package/src/limiters/abstract-rate-limiter.ts +0 -206
  102. package/src/limiters/composite.policy.ts +0 -102
  103. package/src/limiters/fixed-window/fixed-window.limiter.ts +0 -121
  104. package/src/limiters/fixed-window/fixed-window.options.ts +0 -29
  105. package/src/limiters/fixed-window/fixed-window.policy.ts +0 -159
  106. package/src/limiters/fixed-window/fixed-window.state.ts +0 -10
  107. package/src/limiters/fixed-window/fixed-window.status.ts +0 -46
  108. package/src/limiters/fixed-window/index.ts +0 -4
  109. package/src/limiters/generic-cell/generic-cell.limiter.ts +0 -108
  110. package/src/limiters/generic-cell/generic-cell.options.ts +0 -23
  111. package/src/limiters/generic-cell/generic-cell.policy.ts +0 -115
  112. package/src/limiters/generic-cell/generic-cell.state.ts +0 -8
  113. package/src/limiters/generic-cell/generic-cell.status.ts +0 -54
  114. package/src/limiters/generic-cell/index.ts +0 -4
  115. package/src/limiters/http-response-based/http-limit-info.extractor.ts +0 -20
  116. package/src/limiters/http-response-based/http-limit.info.ts +0 -41
  117. package/src/limiters/http-response-based/http-response-based-limiter.options.ts +0 -18
  118. package/src/limiters/http-response-based/http-response-based-limiter.state.ts +0 -13
  119. package/src/limiters/http-response-based/http-response-based-limiter.status.ts +0 -74
  120. package/src/limiters/http-response-based/http-response-based.limiter.ts +0 -512
  121. package/src/limiters/http-response-based/index.ts +0 -6
  122. package/src/limiters/leaky-bucket/index.ts +0 -4
  123. package/src/limiters/leaky-bucket/leaky-bucket.limiter.ts +0 -105
  124. package/src/limiters/leaky-bucket/leaky-bucket.options.ts +0 -23
  125. package/src/limiters/leaky-bucket/leaky-bucket.policy.ts +0 -134
  126. package/src/limiters/leaky-bucket/leaky-bucket.state.ts +0 -9
  127. package/src/limiters/leaky-bucket/leaky-bucket.status.ts +0 -36
  128. package/src/limiters/sliding-window-counter/index.ts +0 -7
  129. package/src/limiters/sliding-window-counter/sliding-window-counter.limiter.ts +0 -76
  130. package/src/limiters/sliding-window-counter/sliding-window-counter.options.ts +0 -20
  131. package/src/limiters/sliding-window-counter/sliding-window-counter.policy.ts +0 -167
  132. package/src/limiters/sliding-window-counter/sliding-window-counter.state.ts +0 -10
  133. package/src/limiters/sliding-window-counter/sliding-window-counter.status.ts +0 -53
  134. package/src/limiters/sliding-window-log/index.ts +0 -4
  135. package/src/limiters/sliding-window-log/sliding-window-log.limiter.ts +0 -65
  136. package/src/limiters/sliding-window-log/sliding-window-log.options.ts +0 -20
  137. package/src/limiters/sliding-window-log/sliding-window-log.policy.ts +0 -166
  138. package/src/limiters/sliding-window-log/sliding-window-log.state.ts +0 -19
  139. package/src/limiters/sliding-window-log/sliding-window-log.status.ts +0 -44
  140. package/src/limiters/token-bucket/index.ts +0 -4
  141. package/src/limiters/token-bucket/token-bucket.limiter.ts +0 -110
  142. package/src/limiters/token-bucket/token-bucket.options.ts +0 -17
  143. package/src/limiters/token-bucket/token-bucket.policy.ts +0 -155
  144. package/src/limiters/token-bucket/token-bucket.state.ts +0 -10
  145. package/src/limiters/token-bucket/token-bucket.status.ts +0 -36
  146. package/src/runtime/default-clock.ts +0 -8
  147. package/src/runtime/execution-tickets.ts +0 -34
  148. package/src/runtime/in-memory-state-store.ts +0 -135
  149. package/src/runtime/rate-limiter.executor.ts +0 -286
  150. package/src/runtime/semaphore.ts +0 -31
  151. package/src/runtime/task.ts +0 -141
  152. package/src/types/limit-behavior.ts +0 -8
  153. package/src/utils/generate-random-string.ts +0 -16
  154. package/src/utils/promise-with-resolvers.ts +0 -23
  155. package/src/utils/sanitize-error.ts +0 -4
  156. package/src/utils/sanitize-priority.ts +0 -22
  157. package/src/utils/validate-cost.ts +0 -16
  158. package/tests/integration/limiters/fixed-window.limiter.spec.ts +0 -371
  159. package/tests/integration/limiters/generic-cell.limiter.spec.ts +0 -361
  160. package/tests/integration/limiters/http-response-based.limiter.spec.ts +0 -833
  161. package/tests/integration/limiters/leaky-bucket.spec.ts +0 -357
  162. package/tests/integration/limiters/sliding-window-counter.limiter.spec.ts +0 -175
  163. package/tests/integration/limiters/sliding-window-log.spec.ts +0 -185
  164. package/tests/integration/limiters/token-bucket.limiter.spec.ts +0 -363
  165. package/tests/tsconfig.json +0 -4
  166. package/tests/unit/policies/composite.policy.spec.ts +0 -244
  167. package/tests/unit/policies/fixed-window.policy.spec.ts +0 -260
  168. package/tests/unit/policies/generic-cell.policy.spec.ts +0 -178
  169. package/tests/unit/policies/leaky-bucket.policy.spec.ts +0 -215
  170. package/tests/unit/policies/sliding-window-counter.policy.spec.ts +0 -209
  171. package/tests/unit/policies/sliding-window-log.policy.spec.ts +0 -285
  172. package/tests/unit/policies/token-bucket.policy.spec.ts +0 -371
  173. package/tests/unit/runtime/execution-tickets.spec.ts +0 -121
  174. package/tests/unit/runtime/in-memory-state-store.spec.ts +0 -238
  175. package/tests/unit/runtime/rate-limiter.executor.spec.ts +0 -353
  176. package/tests/unit/runtime/semaphore.spec.ts +0 -98
  177. package/tests/unit/runtime/task.spec.ts +0 -182
  178. package/tests/unit/utils/generate-random-string.spec.ts +0 -51
  179. package/tests/unit/utils/promise-with-resolvers.spec.ts +0 -57
  180. package/tests/unit/utils/sanitize-priority.spec.ts +0 -46
  181. package/tests/unit/utils/validate-cost.spec.ts +0 -48
  182. package/tsconfig.json +0 -14
  183. package/vitest.config.js +0 -22
@@ -1,134 +0,0 @@
1
- import { type LeakyBucketState } from './leaky-bucket.state.js';
2
- import { type LeakyBucketStatus } from './leaky-bucket.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 LeakyBucketPolicy implements RateLimitPolicy<LeakyBucketState, LeakyBucketStatus> {
8
- constructor(
9
- private readonly _capacity: number,
10
- private readonly _leakRate: number,
11
- private readonly _maxOverflow: number = Number.POSITIVE_INFINITY,
12
- ) {
13
- if (!Number.isFinite(_capacity) || !Number.isSafeInteger(_capacity) || _capacity <= 0) {
14
- throw new Error(`Invalid capacity: ${_capacity}. Must be a positive integer.`);
15
- }
16
-
17
- if (!Number.isFinite(_leakRate) || _leakRate <= 0) {
18
- throw new Error(`Invalid leakRate: ${_leakRate}. Must be a positive number.`);
19
- }
20
-
21
- if (_maxOverflow < 0 || (!Number.isSafeInteger(_maxOverflow) && _maxOverflow !== Number.POSITIVE_INFINITY)) {
22
- throw new Error(`Invalid maxOverflow: ${_maxOverflow}. Must be a non-negative integer or Infinity.`);
23
- }
24
- }
25
-
26
- public get capacity(): number {
27
- return this._capacity;
28
- }
29
-
30
- public get leakRate(): number {
31
- return this._leakRate;
32
- }
33
-
34
- public getInitialState(): LeakyBucketState {
35
- return { level: 0, lastUpdate: 0 };
36
- }
37
-
38
- public getStatus(state: LeakyBucketState, now: number): LeakyBucketStatus {
39
- const { level } = this._syncState(state, now);
40
-
41
- const volumeToClearForOne = Math.max(0, level + 1 - this._capacity);
42
- const nextAvailableAt =
43
- volumeToClearForOne === 0 ? now : now + Math.ceil((volumeToClearForOne / this._leakRate) * 1000);
44
-
45
- const resetAt = level === 0 ? now : now + Math.ceil((level / this._leakRate) * 1000);
46
-
47
- return {
48
- capacity: this._capacity,
49
- leakRate: this._leakRate,
50
- level,
51
- remaining: Math.max(0, this._capacity - level),
52
- nextAvailableAt,
53
- resetAt,
54
- };
55
- }
56
-
57
- public evaluate(
58
- state: LeakyBucketState,
59
- now: number,
60
- cost: number,
61
- shouldReserve?: boolean,
62
- ): RateLimitPolicyResult<LeakyBucketState> {
63
- validateCost(cost, this._capacity);
64
-
65
- const { level, lastUpdate } = this._syncState(state, now);
66
- const nextLevel = level + cost;
67
-
68
- if (nextLevel <= this._capacity) {
69
- return {
70
- decision: { kind: 'allow' },
71
- nextState: { level: nextLevel, lastUpdate },
72
- };
73
- }
74
-
75
- const overflow = nextLevel - this._capacity;
76
-
77
- if (shouldReserve) {
78
- if (overflow > this._maxOverflow) {
79
- const volumeToLeak = nextLevel - (this._capacity + this._maxOverflow);
80
- const waitMs = Math.ceil((volumeToLeak / this._leakRate) * 1000);
81
- const retryAt = now + waitMs;
82
-
83
- return this._deny(level, lastUpdate, retryAt);
84
- }
85
-
86
- const waitMs = Math.ceil((overflow / this._leakRate) * 1000);
87
- const runAt = now + waitMs;
88
-
89
- return {
90
- decision: { kind: 'delay', runAt },
91
- nextState: { level: nextLevel, lastUpdate },
92
- };
93
- }
94
-
95
- const waitMs = Math.ceil((overflow / this._leakRate) * 1000);
96
- const retryAt = now + waitMs;
97
-
98
- return this._deny(level, lastUpdate, retryAt);
99
- }
100
-
101
- public revert(state: LeakyBucketState, cost: number, now: number): LeakyBucketState {
102
- const { level, lastUpdate } = this._syncState(state, now);
103
-
104
- return {
105
- level: Math.max(0, level - cost),
106
- lastUpdate,
107
- };
108
- }
109
-
110
- private _deny(level: number, lastUpdate: number, retryAt: number): RateLimitPolicyResult<LeakyBucketState> {
111
- return {
112
- decision: { kind: 'deny', retryAt },
113
- nextState: { level, lastUpdate },
114
- };
115
- }
116
-
117
- private _syncState(state: LeakyBucketState, now: number): LeakyBucketState {
118
- if (state.lastUpdate === 0) {
119
- return { level: 0, lastUpdate: now };
120
- }
121
-
122
- if (now <= state.lastUpdate) {
123
- return state;
124
- }
125
-
126
- const elapsedMs = now - state.lastUpdate;
127
- const leaked = (elapsedMs / 1000) * this._leakRate;
128
-
129
- return {
130
- level: Math.max(0, state.level - leaked),
131
- lastUpdate: now,
132
- };
133
- }
134
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * Leaky Bucket rate limiter state.
3
- *
4
- * When using a distributed state store, make sure it properly serializes and deserializes the state.
5
- */
6
- export interface LeakyBucketState {
7
- level: number;
8
- lastUpdate: number;
9
- }
@@ -1,36 +0,0 @@
1
- import { type RateLimiterStatus } from '../../core/rate-limiter-status.js';
2
-
3
- /**
4
- * The status of the Leaky Bucket rate limiter.
5
- */
6
- export interface LeakyBucketStatus extends RateLimiterStatus {
7
- /**
8
- * The maximum capacity of the bucket (maximum number of requests that can be queued).
9
- */
10
- capacity: number;
11
-
12
- /**
13
- * The rate at which requests leak from the bucket (requests per second).
14
- */
15
- leakRate: number;
16
-
17
- /**
18
- * The current number of requests in the bucket waiting to be processed.
19
- */
20
- level: number;
21
-
22
- /**
23
- * The number of requests that can still be added to the bucket before reaching capacity.
24
- */
25
- remaining: number;
26
-
27
- /**
28
- * Timestamp (in milliseconds) when the next request slot will become available.
29
- */
30
- nextAvailableAt: number;
31
-
32
- /**
33
- * Timestamp (in milliseconds) when all queued requests will have leaked (bucket becomes empty).
34
- */
35
- resetAt: number;
36
- }
@@ -1,7 +0,0 @@
1
- export type { SlidingWindowCounterState } from './sliding-window-counter.state.js';
2
- export type { SlidingWindowCounterStatus } from './sliding-window-counter.status.js';
3
- export type { SlidingWindowCounterOptions } from './sliding-window-counter.options.js';
4
- export {
5
- type SlidingWindowCounterLimiterRunOptions,
6
- SlidingWindowCounterLimiter,
7
- } from './sliding-window-counter.limiter.js';
@@ -1,76 +0,0 @@
1
- import { type SlidingWindowCounterOptions } from './sliding-window-counter.options.js';
2
- import { SlidingWindowCounterPolicy } from './sliding-window-counter.policy.js';
3
- import { type SlidingWindowCounterState } from './sliding-window-counter.state.js';
4
- import { type SlidingWindowCounterStatus } from './sliding-window-counter.status.js';
5
- import { RateLimitErrorCode } from '../../enums/rate-limit-error-code.js';
6
- import { RateLimitError } from '../../errors/rate-limit.error.js';
7
- import { type RateLimiterRunOptions } from '../../interfaces/rate-limiter-run-options.js';
8
- import { AbstractRateLimiter, type ExecutionContext } from '../abstract-rate-limiter.js';
9
-
10
- /**
11
- * The options for running a task in the Sliding Window Counter rate limiter.
12
- */
13
- export type SlidingWindowCounterLimiterRunOptions = Omit<
14
- RateLimiterRunOptions,
15
- 'limitBehavior' | 'priority' | 'maxWaitMs'
16
- >;
17
-
18
- /**
19
- * Sliding Window Counter rate limiter.
20
- *
21
- * Designed primarily for client-side use to respect third-party limits or protect resources.
22
- * While this can be used as a server-side limiter with custom distributed storage
23
- * (e.g., Redis), it is best-effort and not recommended due to high network round-trip latency.
24
- *
25
- * Note: Unlike queue-based limiters, this implementation operates in strict immediate mode.
26
- * It does not support request queueing, delays, priorities, or task cancellation.
27
- */
28
- export class SlidingWindowCounterLimiter extends AbstractRateLimiter<
29
- SlidingWindowCounterState,
30
- SlidingWindowCounterStatus
31
- > {
32
- protected override readonly _policy: SlidingWindowCounterPolicy;
33
-
34
- private readonly _storeTtl: number;
35
-
36
- constructor(options: SlidingWindowCounterOptions) {
37
- super(options);
38
-
39
- this._policy = new SlidingWindowCounterPolicy(options.limit, options.windowMs);
40
- this._storeTtl = options.windowMs * 2;
41
- }
42
-
43
- protected override async _runInternal<T>(fn: () => T | Promise<T>, ctx: ExecutionContext): Promise<T> {
44
- const now = this._clock.now();
45
-
46
- await this._store.acquireLock?.(ctx.key);
47
-
48
- try {
49
- const state = (await this._store.get(ctx.key)) ?? this._policy.getInitialState();
50
-
51
- const { decision, nextState } = this._policy.evaluate(state, now, ctx.cost);
52
-
53
- if (decision.kind === 'deny') {
54
- this._shouldPrintDebug &&
55
- this._logger.debug(`[DENY] [id: ${ctx.id}, key: ${ctx.key}] - Retry: +${decision.retryAt - now}ms`);
56
-
57
- throw new RateLimitError(RateLimitErrorCode.LimitExceeded, decision.retryAt);
58
- }
59
-
60
- await this._store.set(ctx.key, nextState, this._storeTtl);
61
-
62
- this._shouldPrintDebug &&
63
- this._logger.debug(
64
- `[ALLOW] [id: ${ctx.id}, key: ${ctx.key}] - ${this._getDebugStateString(nextState)}`,
65
- );
66
- } finally {
67
- await this._store.releaseLock?.(ctx.key);
68
- }
69
-
70
- return await this._execute(fn, now, this._storeTtl, ctx);
71
- }
72
-
73
- protected override _getDebugStateString(state: SlidingWindowCounterState): string {
74
- return `lim: ${this._policy.limit}; c/p: ${state.currentCount}/${state.previousCount}`;
75
- }
76
- }
@@ -1,20 +0,0 @@
1
- import { type SlidingWindowCounterState } from './sliding-window-counter.state.js';
2
- import { type RateLimiterOptions } from '../../interfaces/rate-limiter-options.js';
3
-
4
- /**
5
- * Options for the Sliding Window Counter rate limiter.
6
- */
7
- export interface SlidingWindowCounterOptions extends Omit<
8
- RateLimiterOptions<SlidingWindowCounterState>,
9
- 'queue' | 'limitBehavior'
10
- > {
11
- /**
12
- * Maximum number of requests allowed within the time window.
13
- */
14
- limit: number;
15
-
16
- /**
17
- * Duration of the time window in milliseconds.
18
- */
19
- windowMs: number;
20
- }
@@ -1,167 +0,0 @@
1
- import { type SlidingWindowCounterState } from './sliding-window-counter.state.js';
2
- import { type SlidingWindowCounterStatus } from './sliding-window-counter.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 SlidingWindowCounterPolicy implements RateLimitPolicy<
8
- SlidingWindowCounterState,
9
- SlidingWindowCounterStatus
10
- > {
11
- constructor(
12
- private readonly _limit: number,
13
- private readonly _windowMs: number,
14
- ) {
15
- if (!Number.isFinite(_limit) || !Number.isInteger(_limit) || _limit < 0) {
16
- throw new Error(`Invalid limit: ${_limit}. Must be a positive integer.`);
17
- }
18
-
19
- if (!Number.isFinite(_windowMs) || !Number.isInteger(_windowMs) || _windowMs <= 0) {
20
- throw new Error(`Invalid windowMs: ${_windowMs}. Must be a positive integer.`);
21
- }
22
- }
23
-
24
- public get limit(): number {
25
- return this._limit;
26
- }
27
-
28
- public get windowMs(): number {
29
- return this._windowMs;
30
- }
31
-
32
- public getInitialState(): SlidingWindowCounterState {
33
- return {
34
- windowStart: 0,
35
- currentCount: 0,
36
- previousCount: 0,
37
- };
38
- }
39
-
40
- public getStatus(state: SlidingWindowCounterState, now: number): SlidingWindowCounterStatus {
41
- const syncedState = this._syncState(state, now);
42
- const { windowStart, previousCount, currentCount } = syncedState;
43
-
44
- const timeIntoCurrentWindow = now - windowStart;
45
- const weight = (this._windowMs - timeIntoCurrentWindow) / this._windowMs;
46
- const estimatedCount = Math.floor(previousCount * weight + currentCount);
47
-
48
- const remaining = Math.max(0, this._limit - estimatedCount);
49
-
50
- const nextAvailableAt = this._calculateAvailableAt(windowStart, previousCount, currentCount, 1, now);
51
-
52
- let resetAt: number;
53
-
54
- if (currentCount > 0) {
55
- resetAt = windowStart + 2 * this._windowMs;
56
- } else if (previousCount > 0) {
57
- resetAt = windowStart + this._windowMs;
58
- } else {
59
- resetAt = now;
60
- }
61
-
62
- return {
63
- limit: this._limit,
64
- windowMs: this._windowMs,
65
- windowStart,
66
- currentCount,
67
- previousCount,
68
- estimatedCount,
69
- remaining,
70
- nextAvailableAt,
71
- resetAt,
72
- };
73
- }
74
-
75
- public evaluate(
76
- state: SlidingWindowCounterState,
77
- now: number,
78
- cost: number,
79
- ): RateLimitPolicyResult<SlidingWindowCounterState> {
80
- validateCost(cost, this._limit);
81
-
82
- const syncedState = this._syncState(state, now);
83
- const { windowStart, previousCount, currentCount } = syncedState;
84
-
85
- const timeIntoCurrentWindow = now - windowStart;
86
- const weight = (this._windowMs - timeIntoCurrentWindow) / this._windowMs;
87
- const estimatedCount = Math.floor(previousCount * weight + currentCount);
88
-
89
- if (estimatedCount + cost <= this._limit) {
90
- return {
91
- decision: { kind: 'allow' },
92
- nextState: {
93
- windowStart,
94
- previousCount,
95
- currentCount: currentCount + cost,
96
- },
97
- };
98
- }
99
-
100
- const availableAt = this._calculateAvailableAt(windowStart, previousCount, currentCount, cost, now);
101
-
102
- return {
103
- decision: { kind: 'deny', retryAt: availableAt },
104
- nextState: syncedState,
105
- };
106
- }
107
-
108
- public revert(state: SlidingWindowCounterState, cost: number, now: number): SlidingWindowCounterState {
109
- if (cost <= 0) {
110
- return state;
111
- }
112
-
113
- const syncedState = this._syncState(state, now);
114
-
115
- return {
116
- ...syncedState,
117
- currentCount: Math.max(0, syncedState.currentCount - cost),
118
- };
119
- }
120
-
121
- private _syncState(state: SlidingWindowCounterState, now: number): SlidingWindowCounterState {
122
- const currentWindowStart = Math.floor(now / this._windowMs) * this._windowMs;
123
-
124
- if (currentWindowStart <= state.windowStart) {
125
- return state;
126
- }
127
-
128
- const windowsPassed = Math.floor((currentWindowStart - state.windowStart) / this._windowMs);
129
-
130
- if (windowsPassed > 1) {
131
- return { windowStart: currentWindowStart, previousCount: 0, currentCount: 0 };
132
- }
133
-
134
- return {
135
- windowStart: currentWindowStart,
136
- previousCount: state.currentCount,
137
- currentCount: 0,
138
- };
139
- }
140
-
141
- private _calculateAvailableAt(windowStart: number, prev: number, curr: number, cost: number, now: number): number {
142
- if (curr + cost <= this._limit) {
143
- if (prev === 0) {
144
- return now;
145
- }
146
-
147
- const numerator = this._windowMs * (prev + curr + cost - this._limit - 1);
148
- const t = Math.max(0, Math.floor(numerator / prev) + 1);
149
- const absoluteTime = windowStart + t;
150
-
151
- return Math.max(now, absoluteTime);
152
- }
153
-
154
- const newPrev = curr;
155
- const newWindowStart = windowStart + this._windowMs;
156
-
157
- if (newPrev === 0) {
158
- return Math.max(now, newWindowStart);
159
- }
160
-
161
- const numerator = this._windowMs * (newPrev + cost - this._limit - 1);
162
- const t = Math.max(0, Math.floor(numerator / newPrev) + 1);
163
- const absoluteTime = newWindowStart + t;
164
-
165
- return Math.max(now, absoluteTime);
166
- }
167
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * Sliding Window Counter rate limiter state.
3
- *
4
- * When using a distributed state store, make sure it properly serializes and deserializes the state.
5
- */
6
- export interface SlidingWindowCounterState {
7
- windowStart: number;
8
- currentCount: number;
9
- previousCount: number;
10
- }
@@ -1,53 +0,0 @@
1
- import { type RateLimiterStatus } from '../../core/rate-limiter-status.js';
2
-
3
- /**
4
- * The status of the Sliding Window Counter rate limiter.
5
- */
6
- export interface SlidingWindowCounterStatus extends RateLimiterStatus {
7
- /**
8
- * Maximum number of requests allowed within the time window.
9
- */
10
- limit: number;
11
-
12
- /**
13
- * Duration of the time window in milliseconds.
14
- */
15
- windowMs: number;
16
-
17
- /**
18
- * Timestamp (in milliseconds) when the current window started.
19
- */
20
- windowStart: number;
21
-
22
- /**
23
- * Number of requests made in the current window.
24
- */
25
- currentCount: number;
26
-
27
- /**
28
- * Number of requests made in the previous window.
29
- */
30
- previousCount: number;
31
-
32
- /**
33
- * Estimated request count based on the sliding window algorithm.
34
- *
35
- * Calculated by combining current and previous window counts proportionally.
36
- */
37
- estimatedCount: number;
38
-
39
- /**
40
- * Number of requests remaining before hitting the limit.
41
- */
42
- remaining: number;
43
-
44
- /**
45
- * Timestamp (in milliseconds) when the next request slot becomes available.
46
- */
47
- nextAvailableAt: number;
48
-
49
- /**
50
- * Timestamp (in milliseconds) when the current window resets.
51
- */
52
- resetAt: number;
53
- }
@@ -1,4 +0,0 @@
1
- export type { SlidingWindowLogEntry, SlidingWindowLogState } from './sliding-window-log.state.js';
2
- export type { SlidingWindowLogStatus } from './sliding-window-log.status.js';
3
- export type { SlidingWindowLogOptions } from './sliding-window-log.options.js';
4
- export { type SlidingWindowLogLimiterRunOptions, SlidingWindowLogLimiter } from './sliding-window-log.limiter.js';
@@ -1,65 +0,0 @@
1
- import { type SlidingWindowLogOptions } from './sliding-window-log.options.js';
2
- import { SlidingWindowLogPolicy } from './sliding-window-log.policy.js';
3
- import { type SlidingWindowLogState } from './sliding-window-log.state.js';
4
- import { type SlidingWindowLogStatus } from './sliding-window-log.status.js';
5
- import { RateLimitErrorCode } from '../../enums/rate-limit-error-code.js';
6
- import { RateLimitError } from '../../errors/rate-limit.error.js';
7
- import { type RateLimiterRunOptions } from '../../interfaces/rate-limiter-run-options.js';
8
- import { AbstractRateLimiter, type ExecutionContext } from '../abstract-rate-limiter.js';
9
-
10
- /**
11
- * The options for running a task in the Sliding Window Log rate limiter.
12
- */
13
- export type SlidingWindowLogLimiterRunOptions = Omit<RateLimiterRunOptions, 'limitBehavior' | 'priority'>;
14
-
15
- /**
16
- * Sliding Window Log rate limiter.
17
- *
18
- * Designed primarily for client-side use to respect third-party limits or protect resources.
19
- * While this can be used as a server-side limiter with custom distributed storage
20
- * (e.g., Redis), it is best-effort and not recommended due to high network round-trip latency.
21
- *
22
- * Note: Unlike queue-based limiters, this implementation operates in strict immediate mode.
23
- * It does not support request queueing, delays, priorities, or task cancellation.
24
- */
25
- export class SlidingWindowLogLimiter extends AbstractRateLimiter<SlidingWindowLogState, SlidingWindowLogStatus> {
26
- protected override readonly _policy: SlidingWindowLogPolicy;
27
-
28
- constructor(options: SlidingWindowLogOptions) {
29
- super(options);
30
-
31
- this._policy = new SlidingWindowLogPolicy(options.limit, options.windowMs);
32
- }
33
-
34
- protected override async _runInternal<T>(fn: () => T | Promise<T>, ctx: ExecutionContext): Promise<T> {
35
- const now = this._clock.now();
36
- const storeTtlMs = this._policy.windowMs;
37
-
38
- await this._store.acquireLock?.(ctx.key);
39
-
40
- try {
41
- const state = (await this._store.get(ctx.key)) ?? this._policy.getInitialState();
42
-
43
- const { decision, nextState } = this._policy.evaluate(state, now, ctx.cost);
44
-
45
- if (decision.kind === 'deny') {
46
- this._logger.debug(`[DENY] [id: ${ctx.id}, key: ${ctx.key}] - Retry: +${decision.retryAt - now}ms`);
47
- throw new RateLimitError(RateLimitErrorCode.LimitExceeded, decision.retryAt);
48
- }
49
-
50
- await this._store.set(ctx.key, nextState, storeTtlMs);
51
-
52
- this._logger.debug(
53
- `[ALLOW] [id: ${ctx.id}, key: ${ctx.key}] - used/lim: ${nextState.totalUsed}/${this._policy.limit} logs: ${nextState.logs.size}`,
54
- );
55
- } finally {
56
- await this._store.releaseLock?.(ctx.key);
57
- }
58
-
59
- return await this._execute<T>(fn, now, storeTtlMs, ctx);
60
- }
61
-
62
- protected override _getDebugStateString(state: SlidingWindowLogState): string {
63
- return `used/lim: ${state.totalUsed}/${this._policy.limit} logs: ${state.logs.size}`;
64
- }
65
- }
@@ -1,20 +0,0 @@
1
- import { type SlidingWindowLogState } from './sliding-window-log.state.js';
2
- import { type RateLimiterOptions } from '../../interfaces/rate-limiter-options.js';
3
-
4
- /**
5
- * Options for the Sliding Window Log rate limiter.
6
- */
7
- export interface SlidingWindowLogOptions extends Omit<
8
- RateLimiterOptions<SlidingWindowLogState>,
9
- 'queue' | 'limitBehavior'
10
- > {
11
- /**
12
- * Maximum number of requests allowed within the time window.
13
- */
14
- limit: number;
15
-
16
- /**
17
- * Duration of the time window in milliseconds.
18
- */
19
- windowMs: number;
20
- }