@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.
Files changed (238) hide show
  1. package/README.md +20 -0
  2. package/lib/core/cancellable.d.ts +5 -0
  3. package/lib/core/cancellable.js +2 -0
  4. package/lib/core/clock.d.ts +10 -0
  5. package/lib/core/clock.js +2 -0
  6. package/{src/core/decision.ts → lib/core/decision.d.ts} +7 -11
  7. package/lib/core/decision.js +2 -0
  8. package/lib/core/rate-limit-policy.d.ts +14 -0
  9. package/lib/core/rate-limit-policy.js +2 -0
  10. package/lib/core/rate-limiter-status.d.ts +14 -0
  11. package/lib/core/rate-limiter-status.js +2 -0
  12. package/lib/core/rate-limiter.d.ts +34 -0
  13. package/lib/core/rate-limiter.js +2 -0
  14. package/lib/core/state-storage.d.ts +46 -0
  15. package/lib/core/state-storage.js +2 -0
  16. package/lib/enums/rate-limit-error-code.d.ts +26 -0
  17. package/lib/enums/rate-limit-error-code.js +27 -0
  18. package/lib/errors/custom.error.d.ts +6 -0
  19. package/lib/errors/custom.error.js +13 -0
  20. package/lib/errors/invalid-cost.error.d.ts +16 -0
  21. package/lib/errors/invalid-cost.error.js +26 -0
  22. package/lib/errors/rate-limit.error.d.ts +37 -0
  23. package/lib/errors/rate-limit.error.js +75 -0
  24. package/lib/errors/rate-limiter-destroyed.error.d.ts +7 -0
  25. package/lib/errors/rate-limiter-destroyed.error.js +9 -0
  26. package/{src/index.ts → lib/index.d.ts} +1 -0
  27. package/lib/index.js +5 -0
  28. package/lib/interfaces/rate-limiter-options.d.ts +76 -0
  29. package/lib/interfaces/rate-limiter-options.js +2 -0
  30. package/lib/interfaces/rate-limiter-queue-options.d.ts +42 -0
  31. package/lib/interfaces/rate-limiter-queue-options.js +2 -0
  32. package/lib/interfaces/rate-limiter-run-options.d.ts +52 -0
  33. package/lib/interfaces/rate-limiter-run-options.js +2 -0
  34. package/lib/limiters/abstract-rate-limiter.d.ts +44 -0
  35. package/lib/limiters/abstract-rate-limiter.js +133 -0
  36. package/lib/limiters/composite.policy.d.ts +15 -0
  37. package/lib/limiters/composite.policy.js +73 -0
  38. package/lib/limiters/fixed-window/fixed-window.limiter.d.ts +33 -0
  39. package/lib/limiters/fixed-window/fixed-window.limiter.js +85 -0
  40. package/lib/limiters/fixed-window/fixed-window.options.d.ts +27 -0
  41. package/lib/limiters/fixed-window/fixed-window.options.js +2 -0
  42. package/lib/limiters/fixed-window/fixed-window.policy.d.ts +19 -0
  43. package/lib/limiters/fixed-window/fixed-window.policy.js +121 -0
  44. package/{src/limiters/fixed-window/fixed-window.state.ts → lib/limiters/fixed-window/fixed-window.state.d.ts} +4 -3
  45. package/lib/limiters/fixed-window/fixed-window.state.js +2 -0
  46. package/lib/limiters/fixed-window/fixed-window.status.d.ts +39 -0
  47. package/lib/limiters/fixed-window/fixed-window.status.js +2 -0
  48. package/{src/limiters/fixed-window/index.ts → lib/limiters/fixed-window/index.d.ts} +1 -0
  49. package/lib/limiters/fixed-window/index.js +2 -0
  50. package/lib/limiters/generic-cell/generic-cell.limiter.d.ts +30 -0
  51. package/lib/limiters/generic-cell/generic-cell.limiter.js +74 -0
  52. package/lib/limiters/generic-cell/generic-cell.options.d.ts +22 -0
  53. package/lib/limiters/generic-cell/generic-cell.options.js +2 -0
  54. package/lib/limiters/generic-cell/generic-cell.policy.d.ts +18 -0
  55. package/lib/limiters/generic-cell/generic-cell.policy.js +87 -0
  56. package/{src/limiters/generic-cell/generic-cell.state.ts → lib/limiters/generic-cell/generic-cell.state.d.ts} +2 -1
  57. package/lib/limiters/generic-cell/generic-cell.state.js +2 -0
  58. package/lib/limiters/generic-cell/generic-cell.status.d.ts +49 -0
  59. package/lib/limiters/generic-cell/generic-cell.status.js +2 -0
  60. package/{src/limiters/generic-cell/index.ts → lib/limiters/generic-cell/index.d.ts} +1 -0
  61. package/lib/limiters/generic-cell/index.js +2 -0
  62. package/{src/limiters/http-response-based/http-limit-info.extractor.ts → lib/limiters/http-response-based/http-limit-info.extractor.d.ts} +2 -6
  63. package/lib/limiters/http-response-based/http-limit-info.extractor.js +2 -0
  64. package/lib/limiters/http-response-based/http-limit.info.d.ts +39 -0
  65. package/lib/limiters/http-response-based/http-limit.info.js +2 -0
  66. 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
  67. package/lib/limiters/http-response-based/http-response-based-limiter.options.js +2 -0
  68. package/lib/limiters/http-response-based/http-response-based-limiter.state.d.ts +14 -0
  69. package/lib/limiters/http-response-based/http-response-based-limiter.state.js +2 -0
  70. package/lib/limiters/http-response-based/http-response-based-limiter.status.d.ts +70 -0
  71. package/lib/limiters/http-response-based/http-response-based-limiter.status.js +2 -0
  72. package/lib/limiters/http-response-based/http-response-based.limiter.d.ts +56 -0
  73. package/lib/limiters/http-response-based/http-response-based.limiter.js +386 -0
  74. package/{src/limiters/http-response-based/index.ts → lib/limiters/http-response-based/index.d.ts} +1 -0
  75. package/lib/limiters/http-response-based/index.js +2 -0
  76. package/{src/limiters/leaky-bucket/index.ts → lib/limiters/leaky-bucket/index.d.ts} +1 -0
  77. package/lib/limiters/leaky-bucket/index.js +2 -0
  78. package/lib/limiters/leaky-bucket/leaky-bucket.limiter.d.ts +30 -0
  79. package/lib/limiters/leaky-bucket/leaky-bucket.limiter.js +75 -0
  80. package/lib/limiters/leaky-bucket/leaky-bucket.options.d.ts +22 -0
  81. package/lib/limiters/leaky-bucket/leaky-bucket.options.js +2 -0
  82. package/lib/limiters/leaky-bucket/leaky-bucket.policy.d.ts +19 -0
  83. package/lib/limiters/leaky-bucket/leaky-bucket.policy.js +101 -0
  84. package/{src/limiters/leaky-bucket/leaky-bucket.state.ts → lib/limiters/leaky-bucket/leaky-bucket.state.d.ts} +3 -2
  85. package/lib/limiters/leaky-bucket/leaky-bucket.state.js +2 -0
  86. package/lib/limiters/leaky-bucket/leaky-bucket.status.d.ts +31 -0
  87. package/lib/limiters/leaky-bucket/leaky-bucket.status.js +2 -0
  88. package/{src/limiters/sliding-window-counter/index.ts → lib/limiters/sliding-window-counter/index.d.ts} +2 -4
  89. package/lib/limiters/sliding-window-counter/index.js +2 -0
  90. package/lib/limiters/sliding-window-counter/sliding-window-counter.limiter.d.ts +28 -0
  91. package/lib/limiters/sliding-window-counter/sliding-window-counter.limiter.js +47 -0
  92. package/lib/limiters/sliding-window-counter/sliding-window-counter.options.d.ts +16 -0
  93. package/lib/limiters/sliding-window-counter/sliding-window-counter.options.js +2 -0
  94. package/lib/limiters/sliding-window-counter/sliding-window-counter.policy.d.ts +18 -0
  95. package/lib/limiters/sliding-window-counter/sliding-window-counter.policy.js +128 -0
  96. package/{src/limiters/sliding-window-counter/sliding-window-counter.state.ts → lib/limiters/sliding-window-counter/sliding-window-counter.state.d.ts} +4 -3
  97. package/lib/limiters/sliding-window-counter/sliding-window-counter.state.js +2 -0
  98. package/lib/limiters/sliding-window-counter/sliding-window-counter.status.d.ts +45 -0
  99. package/lib/limiters/sliding-window-counter/sliding-window-counter.status.js +2 -0
  100. package/{src/limiters/sliding-window-log/index.ts → lib/limiters/sliding-window-log/index.d.ts} +1 -0
  101. package/lib/limiters/sliding-window-log/index.js +2 -0
  102. package/lib/limiters/sliding-window-log/sliding-window-log.limiter.d.ts +27 -0
  103. package/lib/limiters/sliding-window-log/sliding-window-log.limiter.js +44 -0
  104. package/lib/limiters/sliding-window-log/sliding-window-log.options.d.ts +16 -0
  105. package/lib/limiters/sliding-window-log/sliding-window-log.options.js +2 -0
  106. package/lib/limiters/sliding-window-log/sliding-window-log.policy.d.ts +18 -0
  107. package/lib/limiters/sliding-window-log/sliding-window-log.policy.js +124 -0
  108. package/{src/limiters/sliding-window-log/sliding-window-log.state.ts → lib/limiters/sliding-window-log/sliding-window-log.state.d.ts} +5 -6
  109. package/lib/limiters/sliding-window-log/sliding-window-log.state.js +2 -0
  110. package/lib/limiters/sliding-window-log/sliding-window-log.status.d.ts +39 -0
  111. package/lib/limiters/sliding-window-log/sliding-window-log.status.js +2 -0
  112. package/{src/limiters/token-bucket/index.ts → lib/limiters/token-bucket/index.d.ts} +1 -0
  113. package/lib/limiters/token-bucket/index.js +2 -0
  114. package/lib/limiters/token-bucket/token-bucket.limiter.d.ts +30 -0
  115. package/lib/limiters/token-bucket/token-bucket.limiter.js +75 -0
  116. package/{src/limiters/token-bucket/token-bucket.options.ts → lib/limiters/token-bucket/token-bucket.options.d.ts} +9 -10
  117. package/lib/limiters/token-bucket/token-bucket.options.js +2 -0
  118. package/lib/limiters/token-bucket/token-bucket.policy.d.ts +19 -0
  119. package/lib/limiters/token-bucket/token-bucket.policy.js +116 -0
  120. package/{src/limiters/token-bucket/token-bucket.state.ts → lib/limiters/token-bucket/token-bucket.state.d.ts} +4 -3
  121. package/lib/limiters/token-bucket/token-bucket.state.js +2 -0
  122. package/lib/limiters/token-bucket/token-bucket.status.d.ts +31 -0
  123. package/lib/limiters/token-bucket/token-bucket.status.js +2 -0
  124. package/lib/runtime/default-clock.d.ts +4 -0
  125. package/lib/runtime/default-clock.js +7 -0
  126. package/lib/runtime/execution-tickets.d.ts +12 -0
  127. package/lib/runtime/execution-tickets.js +27 -0
  128. package/lib/runtime/in-memory-state-store.d.ts +19 -0
  129. package/lib/runtime/in-memory-state-store.js +97 -0
  130. package/lib/runtime/rate-limiter.executor.d.ts +47 -0
  131. package/lib/runtime/rate-limiter.executor.js +196 -0
  132. package/lib/runtime/semaphore.d.ts +9 -0
  133. package/lib/runtime/semaphore.js +28 -0
  134. package/lib/runtime/task.d.ts +41 -0
  135. package/lib/runtime/task.js +101 -0
  136. package/{src/types/limit-behavior.ts → lib/types/limit-behavior.d.ts} +1 -0
  137. package/lib/types/limit-behavior.js +2 -0
  138. package/lib/utils/generate-random-string.d.ts +3 -0
  139. package/lib/utils/generate-random-string.js +13 -0
  140. package/lib/utils/promise-with-resolvers.d.ts +9 -0
  141. package/lib/utils/promise-with-resolvers.js +15 -0
  142. package/lib/utils/sanitize-error.d.ts +3 -0
  143. package/lib/utils/sanitize-error.js +5 -0
  144. package/lib/utils/sanitize-priority.d.ts +4 -0
  145. package/lib/utils/sanitize-priority.js +18 -0
  146. package/lib/utils/validate-cost.d.ts +3 -0
  147. package/lib/utils/validate-cost.js +14 -0
  148. package/package.json +13 -2
  149. package/.editorconfig +0 -21
  150. package/.github/workflows/node.yml +0 -87
  151. package/.husky/commit-msg +0 -1
  152. package/.husky/pre-commit +0 -1
  153. package/.megaignore +0 -8
  154. package/.prettierignore +0 -3
  155. package/commitlint.config.js +0 -8
  156. package/eslint.config.js +0 -65
  157. package/lint-staged.config.js +0 -4
  158. package/prettier.config.cjs +0 -1
  159. package/src/core/cancellable.ts +0 -4
  160. package/src/core/clock.ts +0 -9
  161. package/src/core/rate-limit-policy.ts +0 -15
  162. package/src/core/rate-limiter-status.ts +0 -14
  163. package/src/core/rate-limiter.ts +0 -37
  164. package/src/core/state-storage.ts +0 -51
  165. package/src/enums/rate-limit-error-code.ts +0 -29
  166. package/src/errors/custom.error.ts +0 -14
  167. package/src/errors/invalid-cost.error.ts +0 -33
  168. package/src/errors/rate-limit.error.ts +0 -91
  169. package/src/errors/rate-limiter-destroyed.error.ts +0 -8
  170. package/src/interfaces/rate-limiter-options.ts +0 -84
  171. package/src/interfaces/rate-limiter-queue-options.ts +0 -45
  172. package/src/interfaces/rate-limiter-run-options.ts +0 -58
  173. package/src/limiters/abstract-rate-limiter.ts +0 -206
  174. package/src/limiters/composite.policy.ts +0 -102
  175. package/src/limiters/fixed-window/fixed-window.limiter.ts +0 -121
  176. package/src/limiters/fixed-window/fixed-window.options.ts +0 -29
  177. package/src/limiters/fixed-window/fixed-window.policy.ts +0 -159
  178. package/src/limiters/fixed-window/fixed-window.status.ts +0 -46
  179. package/src/limiters/generic-cell/generic-cell.limiter.ts +0 -108
  180. package/src/limiters/generic-cell/generic-cell.options.ts +0 -23
  181. package/src/limiters/generic-cell/generic-cell.policy.ts +0 -115
  182. package/src/limiters/generic-cell/generic-cell.status.ts +0 -54
  183. package/src/limiters/http-response-based/http-limit.info.ts +0 -41
  184. package/src/limiters/http-response-based/http-response-based-limiter.state.ts +0 -13
  185. package/src/limiters/http-response-based/http-response-based-limiter.status.ts +0 -74
  186. package/src/limiters/http-response-based/http-response-based.limiter.ts +0 -512
  187. package/src/limiters/leaky-bucket/leaky-bucket.limiter.ts +0 -105
  188. package/src/limiters/leaky-bucket/leaky-bucket.options.ts +0 -23
  189. package/src/limiters/leaky-bucket/leaky-bucket.policy.ts +0 -134
  190. package/src/limiters/leaky-bucket/leaky-bucket.status.ts +0 -36
  191. package/src/limiters/sliding-window-counter/sliding-window-counter.limiter.ts +0 -76
  192. package/src/limiters/sliding-window-counter/sliding-window-counter.options.ts +0 -20
  193. package/src/limiters/sliding-window-counter/sliding-window-counter.policy.ts +0 -167
  194. package/src/limiters/sliding-window-counter/sliding-window-counter.status.ts +0 -53
  195. package/src/limiters/sliding-window-log/sliding-window-log.limiter.ts +0 -65
  196. package/src/limiters/sliding-window-log/sliding-window-log.options.ts +0 -20
  197. package/src/limiters/sliding-window-log/sliding-window-log.policy.ts +0 -166
  198. package/src/limiters/sliding-window-log/sliding-window-log.status.ts +0 -44
  199. package/src/limiters/token-bucket/token-bucket.limiter.ts +0 -110
  200. package/src/limiters/token-bucket/token-bucket.policy.ts +0 -155
  201. package/src/limiters/token-bucket/token-bucket.status.ts +0 -36
  202. package/src/runtime/default-clock.ts +0 -8
  203. package/src/runtime/execution-tickets.ts +0 -34
  204. package/src/runtime/in-memory-state-store.ts +0 -135
  205. package/src/runtime/rate-limiter.executor.ts +0 -286
  206. package/src/runtime/semaphore.ts +0 -31
  207. package/src/runtime/task.ts +0 -141
  208. package/src/utils/generate-random-string.ts +0 -16
  209. package/src/utils/promise-with-resolvers.ts +0 -23
  210. package/src/utils/sanitize-error.ts +0 -4
  211. package/src/utils/sanitize-priority.ts +0 -22
  212. package/src/utils/validate-cost.ts +0 -16
  213. package/tests/integration/limiters/fixed-window.limiter.spec.ts +0 -371
  214. package/tests/integration/limiters/generic-cell.limiter.spec.ts +0 -361
  215. package/tests/integration/limiters/http-response-based.limiter.spec.ts +0 -833
  216. package/tests/integration/limiters/leaky-bucket.spec.ts +0 -357
  217. package/tests/integration/limiters/sliding-window-counter.limiter.spec.ts +0 -175
  218. package/tests/integration/limiters/sliding-window-log.spec.ts +0 -185
  219. package/tests/integration/limiters/token-bucket.limiter.spec.ts +0 -363
  220. package/tests/tsconfig.json +0 -4
  221. package/tests/unit/policies/composite.policy.spec.ts +0 -244
  222. package/tests/unit/policies/fixed-window.policy.spec.ts +0 -260
  223. package/tests/unit/policies/generic-cell.policy.spec.ts +0 -178
  224. package/tests/unit/policies/leaky-bucket.policy.spec.ts +0 -215
  225. package/tests/unit/policies/sliding-window-counter.policy.spec.ts +0 -209
  226. package/tests/unit/policies/sliding-window-log.policy.spec.ts +0 -285
  227. package/tests/unit/policies/token-bucket.policy.spec.ts +0 -371
  228. package/tests/unit/runtime/execution-tickets.spec.ts +0 -121
  229. package/tests/unit/runtime/in-memory-state-store.spec.ts +0 -238
  230. package/tests/unit/runtime/rate-limiter.executor.spec.ts +0 -353
  231. package/tests/unit/runtime/semaphore.spec.ts +0 -98
  232. package/tests/unit/runtime/task.spec.ts +0 -182
  233. package/tests/unit/utils/generate-random-string.spec.ts +0 -51
  234. package/tests/unit/utils/promise-with-resolvers.spec.ts +0 -57
  235. package/tests/unit/utils/sanitize-priority.spec.ts +0 -46
  236. package/tests/unit/utils/validate-cost.spec.ts +0 -48
  237. package/tsconfig.json +0 -14
  238. package/vitest.config.js +0 -22
@@ -1,286 +0,0 @@
1
- import { BinaryHeap } from '@stimulcross/ds-binary-heap';
2
- import { PolicyPriorityQueue, type Priority, type SelectionPolicy } from '@stimulcross/ds-policy-priority-queue';
3
- import { type Logger, LogLevel } from '@stimulcross/logger';
4
- import { ExecutionTickets } from './execution-tickets.js';
5
- import { Semaphore } from './semaphore.js';
6
- import { Task } from './task.js';
7
- import { type Clock } from '../core/clock.js';
8
- import { RateLimitErrorCode } from '../enums/rate-limit-error-code.js';
9
- import { RateLimitError } from '../errors/rate-limit.error.js';
10
-
11
- /** @internal */
12
- export interface RateLimiterExecutionOptions {
13
- id: string;
14
- key: string;
15
- expiresAt?: number;
16
- priority?: number;
17
- signal?: AbortSignal;
18
- }
19
-
20
- /** @internal */
21
- export interface RateLimiterExecutorQueueOptions {
22
- concurrency?: number;
23
- capacity?: number;
24
- selectionPolicy?: SelectionPolicy;
25
- signal?: AbortSignal;
26
- }
27
-
28
- /** @internal */
29
- export class RateLimiterExecutor {
30
- private readonly _clock: Clock;
31
- private readonly _tickets = new ExecutionTickets();
32
- private readonly _semaphore: Semaphore;
33
- private readonly _queue: PolicyPriorityQueue<Task>;
34
- private readonly _expiryHeap = new BinaryHeap<Task>((a: Task, b: Task): number => a.expiresAt! - b.expiresAt!);
35
-
36
- private _drainTimer: ReturnType<typeof setTimeout> | null = null;
37
- private _expiryTimer: ReturnType<typeof setTimeout> | null = null;
38
- private _nextExpiryScheduledAt: number | null = null;
39
-
40
- constructor(
41
- private readonly _logger: Logger,
42
- clock: Clock,
43
- { concurrency, capacity, selectionPolicy }: RateLimiterExecutorQueueOptions = {},
44
- ) {
45
- this._clock = clock;
46
- this._semaphore = new Semaphore(concurrency ?? null);
47
- this._queue = new PolicyPriorityQueue<Task>({
48
- capacity,
49
- selectionPolicy: selectionPolicy as SelectionPolicy<Task> | undefined,
50
- });
51
- }
52
-
53
- public get isQueueFull(): boolean {
54
- return this._queue.isFull;
55
- }
56
-
57
- public get queueSize(): number {
58
- return this._queue.size;
59
- }
60
-
61
- public get queueCapacity(): number {
62
- return this._queue.capacity;
63
- }
64
-
65
- public async execute<T>(fn: () => T | Promise<T>, runAt: number, options: RateLimiterExecutionOptions): Promise<T> {
66
- const task = new Task<T>(fn, options);
67
-
68
- task.isCancellable &&
69
- task.onAbort(() => {
70
- this._shouldPrintDebug &&
71
- this._logger.debug(
72
- `[DROP CANCELLED] [id: ${options.id}, key: ${options.key}] - ${this._getStateDebugString(task.priority)}`,
73
- );
74
-
75
- this._tickets.dropLast();
76
-
77
- const priorityQueue = this._queue.getQueue(task.priority);
78
- priorityQueue.remove(task);
79
-
80
- this._drain();
81
- });
82
-
83
- this._tickets.add(runAt);
84
- this._queue.enqueue(task, task.priority);
85
-
86
- if (task.expiresAt !== undefined) {
87
- this._expiryHeap.push(task);
88
- }
89
-
90
- this._shouldPrintDebug &&
91
- this._logger.debug(
92
- `↓ [ENQ] [id: ${options.id}, key: ${options.key}] - ${this._getStateDebugString(task.priority)}`,
93
- );
94
-
95
- this._drain();
96
-
97
- return await task;
98
- }
99
-
100
- public clear(): void {
101
- this._clearDrainTimer();
102
- this._clearExpiryTimer();
103
- this._tickets.clear();
104
- this._expiryHeap.clear();
105
-
106
- const pendingTasks = this._drainRemainingTasks();
107
-
108
- if (pendingTasks.length === 0) {
109
- return;
110
- }
111
-
112
- for (const task of pendingTasks) {
113
- this._shouldPrintDebug &&
114
- this._logger.debug(
115
- `[DROP CLEAR] [id: ${task.id}, key: ${task.key}] - Destroy due to clear() - ${this._getStateDebugString(task.priority)}`,
116
- );
117
-
118
- task.destroy();
119
- task.reject(new RateLimitError(RateLimitErrorCode.Destroyed));
120
- }
121
- }
122
-
123
- private get _shouldPrintDebug(): boolean {
124
- return this._logger.minLevel >= LogLevel.DEBUG;
125
- }
126
-
127
- private _drain(): void {
128
- const now = this._clock.now();
129
- const expiredTasks = this._extractExpiredTasks(now);
130
-
131
- for (const task of expiredTasks) {
132
- this._shouldPrintDebug &&
133
- this._logger.debug(
134
- `[DROP EXPIRED] [id: ${task.id}, key: ${task.key}] - ${this._getStateDebugString(task.priority)}`,
135
- );
136
-
137
- this._tickets.dropLast();
138
- task.destroy();
139
- task.reject(new RateLimitError(RateLimitErrorCode.Expired));
140
- }
141
-
142
- this._recalibrateExpiryTimer(now);
143
-
144
- while (!this._queue.isEmpty) {
145
- const nextTicketAt = this._tickets.peek();
146
-
147
- if (nextTicketAt !== undefined && nextTicketAt > now) {
148
- this._scheduleDrainTimer(nextTicketAt - now);
149
- return;
150
- }
151
-
152
- const isAcquired = this._semaphore.acquire();
153
-
154
- if (!isAcquired) {
155
- return;
156
- }
157
-
158
- const task = this._queue.dequeue();
159
-
160
- if (!task) {
161
- this._semaphore.release();
162
- return;
163
- }
164
-
165
- task.destroy();
166
- this._tickets.consume();
167
-
168
- this._shouldPrintDebug &&
169
- this._logger.debug(
170
- `↑ [DEQ] [id: ${task.id}, key: ${task.key}] - ${this._getStateDebugString(task.priority)}`,
171
- );
172
-
173
- void task.run().finally(() => {
174
- this._semaphore.release();
175
- queueMicrotask(() => this._drain());
176
- });
177
- }
178
- }
179
-
180
- private _getNextExpiryTimestamp(): number | null {
181
- while (!this._expiryHeap.isEmpty) {
182
- const task = this._expiryHeap.peek()!;
183
-
184
- if (!task.isActive) {
185
- this._expiryHeap.pop();
186
- continue;
187
- }
188
-
189
- return task.expiresAt!;
190
- }
191
-
192
- return null;
193
- }
194
-
195
- private _extractExpiredTasks(now: number): Task[] {
196
- const result: Task[] = [];
197
-
198
- while (!this._expiryHeap.isEmpty) {
199
- const task = this._expiryHeap.peek()!;
200
-
201
- if (!task.isActive) {
202
- this._expiryHeap.pop();
203
- continue;
204
- }
205
-
206
- if (task.expiresAt! > now) {
207
- break;
208
- }
209
-
210
- this._expiryHeap.pop();
211
-
212
- const priorityQueue = this._queue.getQueue(task.priority);
213
- priorityQueue.remove(task);
214
- result.push(task);
215
- }
216
-
217
- return result;
218
- }
219
-
220
- private _drainRemainingTasks(): Task[] {
221
- const remaining: Task[] = [];
222
-
223
- for (const queue of this._queue.queues()) {
224
- while (!queue.isEmpty) {
225
- const task = queue.dequeue()!;
226
- remaining.push(task);
227
- }
228
- }
229
-
230
- return remaining;
231
- }
232
-
233
- private _scheduleDrainTimer(delayMs: number): void {
234
- if (this._drainTimer) {
235
- return;
236
- }
237
-
238
- this._drainTimer = setTimeout(() => {
239
- this._clearDrainTimer();
240
- this._drain();
241
- }, delayMs);
242
- }
243
-
244
- private _recalibrateExpiryTimer(now: number): void {
245
- const nextExpiry = this._getNextExpiryTimestamp();
246
-
247
- if (nextExpiry === null) {
248
- this._clearExpiryTimer();
249
- this._nextExpiryScheduledAt = null;
250
-
251
- return;
252
- }
253
-
254
- if (this._nextExpiryScheduledAt === nextExpiry) {
255
- return;
256
- }
257
-
258
- this._clearExpiryTimer();
259
-
260
- const delay = Math.max(0, nextExpiry - now);
261
-
262
- this._nextExpiryScheduledAt = nextExpiry;
263
- this._expiryTimer = setTimeout(() => {
264
- this._clearExpiryTimer();
265
- this._drain();
266
- }, delay);
267
- }
268
-
269
- private _clearDrainTimer(): void {
270
- if (this._drainTimer) {
271
- clearTimeout(this._drainTimer);
272
- this._drainTimer = null;
273
- }
274
- }
275
-
276
- private _clearExpiryTimer(): void {
277
- if (this._expiryTimer) {
278
- clearTimeout(this._expiryTimer);
279
- this._expiryTimer = null;
280
- }
281
- }
282
-
283
- private _getStateDebugString(priority: Priority): string {
284
- return `prt: ${priority} | q: ${this._queue.size}/${this._queue.capacity}`;
285
- }
286
- }
@@ -1,31 +0,0 @@
1
- /** @internal */
2
- export class Semaphore {
3
- private _permits: number;
4
-
5
- constructor(private readonly _maxPermits: number | null) {
6
- if (_maxPermits !== null && (!Number.isSafeInteger(_maxPermits) || _maxPermits <= 0)) {
7
- throw new Error('Maximum permits must be a non-negative integer or null');
8
- }
9
-
10
- this._permits = _maxPermits ?? 0;
11
- }
12
-
13
- public acquire(): boolean {
14
- if (this._maxPermits === null) {
15
- return true;
16
- }
17
-
18
- if (this._permits > 0) {
19
- this._permits--;
20
- return true;
21
- }
22
-
23
- return false;
24
- }
25
-
26
- public release(): void {
27
- if (this._maxPermits !== null && this._permits < this._maxPermits) {
28
- this._permits++;
29
- }
30
- }
31
- }
@@ -1,141 +0,0 @@
1
- import { Priority } from '@stimulcross/ds-policy-priority-queue';
2
- import { RateLimitErrorCode } from '../enums/rate-limit-error-code.js';
3
- import { RateLimitError } from '../errors/rate-limit.error.js';
4
- import { promiseWithResolvers } from '../utils/promise-with-resolvers.js';
5
-
6
- /** @internal */
7
- export interface TaskOptions {
8
- id: string;
9
- key: string;
10
- priority?: Priority;
11
- expiresAt?: number;
12
- signal?: AbortSignal;
13
- }
14
-
15
- /** @internal */
16
- export class Task<T = any> implements PromiseLike<T> {
17
- private readonly _task: () => T | Promise<T>;
18
- private readonly _promise: Promise<T>;
19
- private readonly _resolve: (v: T) => void;
20
- private readonly _reject: (reason?: any) => void;
21
-
22
- private readonly _id: string;
23
- private readonly _key: string;
24
- private readonly _priority: Priority;
25
- private readonly _expiresAt?: number;
26
-
27
- private _signal?: AbortSignal;
28
- private _abortListener?: () => void;
29
- private _abortHandler?: () => void;
30
-
31
- private _isActive = true;
32
- private _isAborted = false;
33
-
34
- constructor(task: () => T | Promise<T>, { id, key, priority = Priority.Normal, expiresAt, signal }: TaskOptions) {
35
- const { promise, resolve, reject } = promiseWithResolvers<T>();
36
-
37
- this._task = task;
38
- this._promise = promise;
39
- this._resolve = resolve;
40
- this._reject = reject;
41
- this._id = id;
42
- this._key = key;
43
- this._priority = priority;
44
- this._expiresAt = expiresAt;
45
-
46
- if (signal) {
47
- this._signal = signal;
48
-
49
- this._abortListener = (): void => {
50
- this._isActive = false;
51
- this._isAborted = true;
52
-
53
- this._abortHandler?.();
54
- this._reject(new RateLimitError(RateLimitErrorCode.Cancelled, undefined, 'Aborted by client'));
55
- this.destroy();
56
- };
57
-
58
- this._signal.addEventListener('abort', this._abortListener, { once: true });
59
- }
60
- }
61
-
62
- public get id(): string {
63
- return this._id;
64
- }
65
-
66
- public get key(): string {
67
- return this._key;
68
- }
69
-
70
- public get priority(): Priority {
71
- return this._priority;
72
- }
73
-
74
- public get expiresAt(): number | undefined {
75
- return this._expiresAt;
76
- }
77
-
78
- public get isActive(): boolean {
79
- return this._isActive;
80
- }
81
-
82
- public get isCancellable(): boolean {
83
- return Boolean(this._signal);
84
- }
85
-
86
- public get isAborted(): boolean {
87
- return this._isAborted;
88
- }
89
-
90
- public async run(): Promise<void> {
91
- this._isActive = false;
92
-
93
- try {
94
- const result = await this._task();
95
- this._resolve(result);
96
- } catch (e) {
97
- this._reject(e);
98
- }
99
- }
100
-
101
- public reject(reason: unknown): void {
102
- this._isActive = false;
103
- this._reject(reason);
104
- }
105
-
106
- public destroy(): void {
107
- this._isActive = false;
108
-
109
- if (this._signal && this._abortListener) {
110
- this._signal.removeEventListener('abort', this._abortListener);
111
-
112
- this._signal = undefined;
113
- this._abortListener = undefined;
114
- }
115
-
116
- if (this._abortHandler) {
117
- this._abortHandler = undefined;
118
- }
119
- }
120
-
121
- public onAbort(handler: () => void): void {
122
- this._abortHandler = handler;
123
- }
124
-
125
- public then<TResult1 = T, TResult2 = never>(
126
- onfulfilled?: ((value: T) => PromiseLike<TResult1> | TResult1) | null,
127
- onrejected?: ((reason: any) => PromiseLike<TResult2> | TResult2) | null,
128
- ): PromiseLike<TResult1 | TResult2> {
129
- return this._promise.then(onfulfilled, onrejected);
130
- }
131
-
132
- public catch<TResult = never>(
133
- onrejected?: ((reason: any) => PromiseLike<TResult> | TResult) | null,
134
- ): PromiseLike<T | TResult> {
135
- return this._promise.catch(onrejected);
136
- }
137
-
138
- public finally(onfinally?: (() => void) | null): PromiseLike<T> {
139
- return this._promise.finally(onfinally);
140
- }
141
- }
@@ -1,16 +0,0 @@
1
- const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
2
-
3
- /** @internal */
4
- export function generateRandomString(length: number = 7): string {
5
- if (!Number.isSafeInteger(length) || length < 0) {
6
- throw new RangeError(`Invalid length: ${length}. Length must be a positive integer.`);
7
- }
8
-
9
- const result = new Array(length);
10
-
11
- for (let i = 0; i < length; i++) {
12
- result.push(characters.charAt(Math.floor(Math.random() * characters.length)));
13
- }
14
-
15
- return result.join('');
16
- }
@@ -1,23 +0,0 @@
1
- /** @internal */
2
- export interface PromiseWithResolvers<T = void> {
3
- promise: Promise<T>;
4
- resolve: (value: T | PromiseLike<T>) => void;
5
- reject: (reason?: unknown) => void;
6
- }
7
-
8
- /** @internal */
9
- export function promiseWithResolvers<T = void>(): PromiseWithResolvers<T> {
10
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
11
- if (Promise.withResolvers) {
12
- return Promise.withResolvers<T>();
13
- }
14
-
15
- let resolve!: (value: T | PromiseLike<T>) => void;
16
- let reject!: (reason: unknown) => void;
17
- const promise = new Promise<T>((_resolve, _reject) => {
18
- resolve = _resolve;
19
- reject = _reject;
20
- });
21
-
22
- return { promise, resolve, reject };
23
- }
@@ -1,4 +0,0 @@
1
- /** @internal */
2
- export function sanitizeError(error: unknown): Error {
3
- return error instanceof Error ? error : new Error('Non-error thrown. Check "cause" property', { cause: error });
4
- }
@@ -1,22 +0,0 @@
1
- import { Priority } from '@stimulcross/ds-policy-priority-queue';
2
-
3
- /** @internal */
4
- export function sanitizePriority(priority: number): Priority {
5
- if (!Number.isFinite(priority)) {
6
- return Priority.Normal;
7
- }
8
-
9
- if (priority < Priority.Lowest) {
10
- return Priority.Lowest;
11
- }
12
-
13
- if (priority > Priority.Highest) {
14
- return Priority.Highest;
15
- }
16
-
17
- if (!Number.isInteger(priority)) {
18
- priority = Math.round(priority);
19
- }
20
-
21
- return priority;
22
- }
@@ -1,16 +0,0 @@
1
- import { InvalidCostError } from '../errors/invalid-cost.error.js';
2
-
3
- /** @internal */
4
- export function validateCost(cost: number, max?: number, min?: number): void {
5
- if (!Number.isSafeInteger(cost) || cost < 0) {
6
- throw new InvalidCostError(`Invalid cost: ${cost}. Cost must be a positive integer.`, cost);
7
- }
8
-
9
- if (max !== undefined && cost > max) {
10
- throw new InvalidCostError(`Invalid cost: ${cost}. Cost must be greater than or equal to ${max}.`, cost);
11
- }
12
-
13
- if (min !== undefined && cost < min) {
14
- throw new InvalidCostError(`Invalid cost: ${cost}. Cost must be greater than or equal to ${min}.`, cost);
15
- }
16
- }