@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
@@ -0,0 +1,196 @@
1
+ import { BinaryHeap } from '@stimulcross/ds-binary-heap';
2
+ import { PolicyPriorityQueue } from '@stimulcross/ds-policy-priority-queue';
3
+ import { 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 { RateLimitErrorCode } from '../enums/rate-limit-error-code.js';
8
+ import { RateLimitError } from '../errors/rate-limit.error.js';
9
+ /** @internal */
10
+ export class RateLimiterExecutor {
11
+ _logger;
12
+ _clock;
13
+ _tickets = new ExecutionTickets();
14
+ _semaphore;
15
+ _queue;
16
+ _expiryHeap = new BinaryHeap((a, b) => a.expiresAt - b.expiresAt);
17
+ _drainTimer = null;
18
+ _expiryTimer = null;
19
+ _nextExpiryScheduledAt = null;
20
+ constructor(_logger, clock, { concurrency, capacity, selectionPolicy } = {}) {
21
+ this._logger = _logger;
22
+ this._clock = clock;
23
+ this._semaphore = new Semaphore(concurrency ?? null);
24
+ this._queue = new PolicyPriorityQueue({
25
+ capacity,
26
+ selectionPolicy: selectionPolicy,
27
+ });
28
+ }
29
+ get isQueueFull() {
30
+ return this._queue.isFull;
31
+ }
32
+ get queueSize() {
33
+ return this._queue.size;
34
+ }
35
+ get queueCapacity() {
36
+ return this._queue.capacity;
37
+ }
38
+ async execute(fn, runAt, options) {
39
+ const task = new Task(fn, options);
40
+ task.isCancellable &&
41
+ task.onAbort(() => {
42
+ this._shouldPrintDebug &&
43
+ this._logger.debug(`[DROP CANCELLED] [id: ${options.id}, key: ${options.key}] - ${this._getStateDebugString(task.priority)}`);
44
+ this._tickets.dropLast();
45
+ const priorityQueue = this._queue.getQueue(task.priority);
46
+ priorityQueue.remove(task);
47
+ this._drain();
48
+ });
49
+ this._tickets.add(runAt);
50
+ this._queue.enqueue(task, task.priority);
51
+ if (task.expiresAt !== undefined) {
52
+ this._expiryHeap.push(task);
53
+ }
54
+ this._shouldPrintDebug &&
55
+ this._logger.debug(`↓ [ENQ] [id: ${options.id}, key: ${options.key}] - ${this._getStateDebugString(task.priority)}`);
56
+ this._drain();
57
+ return await task;
58
+ }
59
+ clear() {
60
+ this._clearDrainTimer();
61
+ this._clearExpiryTimer();
62
+ this._tickets.clear();
63
+ this._expiryHeap.clear();
64
+ const pendingTasks = this._drainRemainingTasks();
65
+ if (pendingTasks.length === 0) {
66
+ return;
67
+ }
68
+ for (const task of pendingTasks) {
69
+ this._shouldPrintDebug &&
70
+ this._logger.debug(`[DROP CLEAR] [id: ${task.id}, key: ${task.key}] - Destroy due to clear() - ${this._getStateDebugString(task.priority)}`);
71
+ task.destroy();
72
+ task.reject(new RateLimitError(RateLimitErrorCode.Destroyed));
73
+ }
74
+ }
75
+ get _shouldPrintDebug() {
76
+ return this._logger.minLevel >= LogLevel.DEBUG;
77
+ }
78
+ _drain() {
79
+ const now = this._clock.now();
80
+ const expiredTasks = this._extractExpiredTasks(now);
81
+ for (const task of expiredTasks) {
82
+ this._shouldPrintDebug &&
83
+ this._logger.debug(`[DROP EXPIRED] [id: ${task.id}, key: ${task.key}] - ${this._getStateDebugString(task.priority)}`);
84
+ this._tickets.dropLast();
85
+ task.destroy();
86
+ task.reject(new RateLimitError(RateLimitErrorCode.Expired));
87
+ }
88
+ this._recalibrateExpiryTimer(now);
89
+ while (!this._queue.isEmpty) {
90
+ const nextTicketAt = this._tickets.peek();
91
+ if (nextTicketAt !== undefined && nextTicketAt > now) {
92
+ this._scheduleDrainTimer(nextTicketAt - now);
93
+ return;
94
+ }
95
+ const isAcquired = this._semaphore.acquire();
96
+ if (!isAcquired) {
97
+ return;
98
+ }
99
+ const task = this._queue.dequeue();
100
+ if (!task) {
101
+ this._semaphore.release();
102
+ return;
103
+ }
104
+ task.destroy();
105
+ this._tickets.consume();
106
+ this._shouldPrintDebug &&
107
+ this._logger.debug(`↑ [DEQ] [id: ${task.id}, key: ${task.key}] - ${this._getStateDebugString(task.priority)}`);
108
+ void task.run().finally(() => {
109
+ this._semaphore.release();
110
+ queueMicrotask(() => this._drain());
111
+ });
112
+ }
113
+ }
114
+ _getNextExpiryTimestamp() {
115
+ while (!this._expiryHeap.isEmpty) {
116
+ const task = this._expiryHeap.peek();
117
+ if (!task.isActive) {
118
+ this._expiryHeap.pop();
119
+ continue;
120
+ }
121
+ return task.expiresAt;
122
+ }
123
+ return null;
124
+ }
125
+ _extractExpiredTasks(now) {
126
+ const result = [];
127
+ while (!this._expiryHeap.isEmpty) {
128
+ const task = this._expiryHeap.peek();
129
+ if (!task.isActive) {
130
+ this._expiryHeap.pop();
131
+ continue;
132
+ }
133
+ if (task.expiresAt > now) {
134
+ break;
135
+ }
136
+ this._expiryHeap.pop();
137
+ const priorityQueue = this._queue.getQueue(task.priority);
138
+ priorityQueue.remove(task);
139
+ result.push(task);
140
+ }
141
+ return result;
142
+ }
143
+ _drainRemainingTasks() {
144
+ const remaining = [];
145
+ for (const queue of this._queue.queues()) {
146
+ while (!queue.isEmpty) {
147
+ const task = queue.dequeue();
148
+ remaining.push(task);
149
+ }
150
+ }
151
+ return remaining;
152
+ }
153
+ _scheduleDrainTimer(delayMs) {
154
+ if (this._drainTimer) {
155
+ return;
156
+ }
157
+ this._drainTimer = setTimeout(() => {
158
+ this._clearDrainTimer();
159
+ this._drain();
160
+ }, delayMs);
161
+ }
162
+ _recalibrateExpiryTimer(now) {
163
+ const nextExpiry = this._getNextExpiryTimestamp();
164
+ if (nextExpiry === null) {
165
+ this._clearExpiryTimer();
166
+ this._nextExpiryScheduledAt = null;
167
+ return;
168
+ }
169
+ if (this._nextExpiryScheduledAt === nextExpiry) {
170
+ return;
171
+ }
172
+ this._clearExpiryTimer();
173
+ const delay = Math.max(0, nextExpiry - now);
174
+ this._nextExpiryScheduledAt = nextExpiry;
175
+ this._expiryTimer = setTimeout(() => {
176
+ this._clearExpiryTimer();
177
+ this._drain();
178
+ }, delay);
179
+ }
180
+ _clearDrainTimer() {
181
+ if (this._drainTimer) {
182
+ clearTimeout(this._drainTimer);
183
+ this._drainTimer = null;
184
+ }
185
+ }
186
+ _clearExpiryTimer() {
187
+ if (this._expiryTimer) {
188
+ clearTimeout(this._expiryTimer);
189
+ this._expiryTimer = null;
190
+ }
191
+ }
192
+ _getStateDebugString(priority) {
193
+ return `prt: ${priority} | q: ${this._queue.size}/${this._queue.capacity}`;
194
+ }
195
+ }
196
+ //# sourceMappingURL=rate-limiter.executor.js.map
@@ -0,0 +1,9 @@
1
+ /** @internal */
2
+ export declare class Semaphore {
3
+ private readonly _maxPermits;
4
+ private _permits;
5
+ constructor(_maxPermits: number | null);
6
+ acquire(): boolean;
7
+ release(): void;
8
+ }
9
+ //# sourceMappingURL=semaphore.d.ts.map
@@ -0,0 +1,28 @@
1
+ /** @internal */
2
+ export class Semaphore {
3
+ _maxPermits;
4
+ _permits;
5
+ constructor(_maxPermits) {
6
+ this._maxPermits = _maxPermits;
7
+ if (_maxPermits !== null && (!Number.isSafeInteger(_maxPermits) || _maxPermits <= 0)) {
8
+ throw new Error('Maximum permits must be a non-negative integer or null');
9
+ }
10
+ this._permits = _maxPermits ?? 0;
11
+ }
12
+ acquire() {
13
+ if (this._maxPermits === null) {
14
+ return true;
15
+ }
16
+ if (this._permits > 0) {
17
+ this._permits--;
18
+ return true;
19
+ }
20
+ return false;
21
+ }
22
+ release() {
23
+ if (this._maxPermits !== null && this._permits < this._maxPermits) {
24
+ this._permits++;
25
+ }
26
+ }
27
+ }
28
+ //# sourceMappingURL=semaphore.js.map
@@ -0,0 +1,41 @@
1
+ import { Priority } from '@stimulcross/ds-policy-priority-queue';
2
+ /** @internal */
3
+ export interface TaskOptions {
4
+ id: string;
5
+ key: string;
6
+ priority?: Priority;
7
+ expiresAt?: number;
8
+ signal?: AbortSignal;
9
+ }
10
+ /** @internal */
11
+ export declare class Task<T = any> implements PromiseLike<T> {
12
+ private readonly _task;
13
+ private readonly _promise;
14
+ private readonly _resolve;
15
+ private readonly _reject;
16
+ private readonly _id;
17
+ private readonly _key;
18
+ private readonly _priority;
19
+ private readonly _expiresAt?;
20
+ private _signal?;
21
+ private _abortListener?;
22
+ private _abortHandler?;
23
+ private _isActive;
24
+ private _isAborted;
25
+ constructor(task: () => T | Promise<T>, { id, key, priority, expiresAt, signal }: TaskOptions);
26
+ get id(): string;
27
+ get key(): string;
28
+ get priority(): Priority;
29
+ get expiresAt(): number | undefined;
30
+ get isActive(): boolean;
31
+ get isCancellable(): boolean;
32
+ get isAborted(): boolean;
33
+ run(): Promise<void>;
34
+ reject(reason: unknown): void;
35
+ destroy(): void;
36
+ onAbort(handler: () => void): void;
37
+ then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => PromiseLike<TResult1> | TResult1) | null, onrejected?: ((reason: any) => PromiseLike<TResult2> | TResult2) | null): PromiseLike<TResult1 | TResult2>;
38
+ catch<TResult = never>(onrejected?: ((reason: any) => PromiseLike<TResult> | TResult) | null): PromiseLike<T | TResult>;
39
+ finally(onfinally?: (() => void) | null): PromiseLike<T>;
40
+ }
41
+ //# sourceMappingURL=task.d.ts.map
@@ -0,0 +1,101 @@
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
+ /** @internal */
6
+ export class Task {
7
+ _task;
8
+ _promise;
9
+ _resolve;
10
+ _reject;
11
+ _id;
12
+ _key;
13
+ _priority;
14
+ _expiresAt;
15
+ _signal;
16
+ _abortListener;
17
+ _abortHandler;
18
+ _isActive = true;
19
+ _isAborted = false;
20
+ constructor(task, { id, key, priority = Priority.Normal, expiresAt, signal }) {
21
+ const { promise, resolve, reject } = promiseWithResolvers();
22
+ this._task = task;
23
+ this._promise = promise;
24
+ this._resolve = resolve;
25
+ this._reject = reject;
26
+ this._id = id;
27
+ this._key = key;
28
+ this._priority = priority;
29
+ this._expiresAt = expiresAt;
30
+ if (signal) {
31
+ this._signal = signal;
32
+ this._abortListener = () => {
33
+ this._isActive = false;
34
+ this._isAborted = true;
35
+ this._abortHandler?.();
36
+ this._reject(new RateLimitError(RateLimitErrorCode.Cancelled, undefined, 'Aborted by client'));
37
+ this.destroy();
38
+ };
39
+ this._signal.addEventListener('abort', this._abortListener, { once: true });
40
+ }
41
+ }
42
+ get id() {
43
+ return this._id;
44
+ }
45
+ get key() {
46
+ return this._key;
47
+ }
48
+ get priority() {
49
+ return this._priority;
50
+ }
51
+ get expiresAt() {
52
+ return this._expiresAt;
53
+ }
54
+ get isActive() {
55
+ return this._isActive;
56
+ }
57
+ get isCancellable() {
58
+ return Boolean(this._signal);
59
+ }
60
+ get isAborted() {
61
+ return this._isAborted;
62
+ }
63
+ async run() {
64
+ this._isActive = false;
65
+ try {
66
+ const result = await this._task();
67
+ this._resolve(result);
68
+ }
69
+ catch (e) {
70
+ this._reject(e);
71
+ }
72
+ }
73
+ reject(reason) {
74
+ this._isActive = false;
75
+ this._reject(reason);
76
+ }
77
+ destroy() {
78
+ this._isActive = false;
79
+ if (this._signal && this._abortListener) {
80
+ this._signal.removeEventListener('abort', this._abortListener);
81
+ this._signal = undefined;
82
+ this._abortListener = undefined;
83
+ }
84
+ if (this._abortHandler) {
85
+ this._abortHandler = undefined;
86
+ }
87
+ }
88
+ onAbort(handler) {
89
+ this._abortHandler = handler;
90
+ }
91
+ then(onfulfilled, onrejected) {
92
+ return this._promise.then(onfulfilled, onrejected);
93
+ }
94
+ catch(onrejected) {
95
+ return this._promise.catch(onrejected);
96
+ }
97
+ finally(onfinally) {
98
+ return this._promise.finally(onfinally);
99
+ }
100
+ }
101
+ //# sourceMappingURL=task.js.map
@@ -6,3 +6,4 @@
6
6
  * - `enqueue` - enqueues the task
7
7
  */
8
8
  export type LimitBehavior = 'enqueue' | 'reject';
9
+ //# sourceMappingURL=limit-behavior.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=limit-behavior.js.map
@@ -0,0 +1,3 @@
1
+ /** @internal */
2
+ export declare function generateRandomString(length?: number): string;
3
+ //# sourceMappingURL=generate-random-string.d.ts.map
@@ -0,0 +1,13 @@
1
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
2
+ /** @internal */
3
+ export function generateRandomString(length = 7) {
4
+ if (!Number.isSafeInteger(length) || length < 0) {
5
+ throw new RangeError(`Invalid length: ${length}. Length must be a positive integer.`);
6
+ }
7
+ const result = new Array(length);
8
+ for (let i = 0; i < length; i++) {
9
+ result.push(characters.charAt(Math.floor(Math.random() * characters.length)));
10
+ }
11
+ return result.join('');
12
+ }
13
+ //# sourceMappingURL=generate-random-string.js.map
@@ -0,0 +1,9 @@
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
+ /** @internal */
8
+ export declare function promiseWithResolvers<T = void>(): PromiseWithResolvers<T>;
9
+ //# sourceMappingURL=promise-with-resolvers.d.ts.map
@@ -0,0 +1,15 @@
1
+ /** @internal */
2
+ export function promiseWithResolvers() {
3
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
4
+ if (Promise.withResolvers) {
5
+ return Promise.withResolvers();
6
+ }
7
+ let resolve;
8
+ let reject;
9
+ const promise = new Promise((_resolve, _reject) => {
10
+ resolve = _resolve;
11
+ reject = _reject;
12
+ });
13
+ return { promise, resolve, reject };
14
+ }
15
+ //# sourceMappingURL=promise-with-resolvers.js.map
@@ -0,0 +1,3 @@
1
+ /** @internal */
2
+ export declare function sanitizeError(error: unknown): Error;
3
+ //# sourceMappingURL=sanitize-error.d.ts.map
@@ -0,0 +1,5 @@
1
+ /** @internal */
2
+ export function sanitizeError(error) {
3
+ return error instanceof Error ? error : new Error('Non-error thrown. Check "cause" property', { cause: error });
4
+ }
5
+ //# sourceMappingURL=sanitize-error.js.map
@@ -0,0 +1,4 @@
1
+ import { Priority } from '@stimulcross/ds-policy-priority-queue';
2
+ /** @internal */
3
+ export declare function sanitizePriority(priority: number): Priority;
4
+ //# sourceMappingURL=sanitize-priority.d.ts.map
@@ -0,0 +1,18 @@
1
+ import { Priority } from '@stimulcross/ds-policy-priority-queue';
2
+ /** @internal */
3
+ export function sanitizePriority(priority) {
4
+ if (!Number.isFinite(priority)) {
5
+ return Priority.Normal;
6
+ }
7
+ if (priority < Priority.Lowest) {
8
+ return Priority.Lowest;
9
+ }
10
+ if (priority > Priority.Highest) {
11
+ return Priority.Highest;
12
+ }
13
+ if (!Number.isInteger(priority)) {
14
+ priority = Math.round(priority);
15
+ }
16
+ return priority;
17
+ }
18
+ //# sourceMappingURL=sanitize-priority.js.map
@@ -0,0 +1,3 @@
1
+ /** @internal */
2
+ export declare function validateCost(cost: number, max?: number, min?: number): void;
3
+ //# sourceMappingURL=validate-cost.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { InvalidCostError } from '../errors/invalid-cost.error.js';
2
+ /** @internal */
3
+ export function validateCost(cost, max, min) {
4
+ if (!Number.isSafeInteger(cost) || cost < 0) {
5
+ throw new InvalidCostError(`Invalid cost: ${cost}. Cost must be a positive integer.`, cost);
6
+ }
7
+ if (max !== undefined && cost > max) {
8
+ throw new InvalidCostError(`Invalid cost: ${cost}. Cost must be greater than or equal to ${max}.`, cost);
9
+ }
10
+ if (min !== undefined && cost < min) {
11
+ throw new InvalidCostError(`Invalid cost: ${cost}. Cost must be greater than or equal to ${min}.`, cost);
12
+ }
13
+ }
14
+ //# sourceMappingURL=validate-cost.js.map
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@stimulcross/rate-limiter",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "A collection of client-side rate limiters for Node.js and browsers.",
5
+ "type": "module",
5
6
  "engines": {
6
7
  "node": ">=20"
7
8
  },
8
- "type": "module",
9
+ "sideEffects": false,
10
+ "repository": "github:stimulcross/logger",
11
+ "author": "Stimul Cross <stimulcross@gmail.com>",
12
+ "license": "MIT",
9
13
  "main": "./lib/index.js",
10
14
  "types": "./lib/index.d.ts",
11
15
  "exports": {
@@ -66,6 +70,13 @@
66
70
  "typescript": "^5.9.3",
67
71
  "vitest": "^4.1.0"
68
72
  },
73
+ "files": [
74
+ "LICENSE",
75
+ "README.md",
76
+ "lib",
77
+ "!lib/**/*.d.ts.map",
78
+ "!lib/**/*.js.map"
79
+ ],
69
80
  "publishConfig": {
70
81
  "access": "public"
71
82
  },
package/.editorconfig DELETED
@@ -1,21 +0,0 @@
1
- root = true
2
-
3
- [*]
4
- end_of_line = lf
5
- insert_final_newline = true
6
- charset = utf-8
7
- indent_style = tab
8
- tab_width = 4
9
- max_line_length = 120
10
-
11
- [*.json]
12
- indent_style = tab
13
- indent_size = 4
14
-
15
- [package.json]
16
- indent_style = space
17
- indent_size = 2
18
-
19
- [*.yml]
20
- indent_style = space
21
- indent_size = 2
@@ -1,87 +0,0 @@
1
- name: Node.js CI
2
-
3
- on:
4
- push:
5
- branches: [ main ]
6
- pull_request:
7
- branches: [ main ]
8
-
9
- jobs:
10
- checks:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - name: Checkout
14
- uses: actions/checkout@v4
15
-
16
- - name: Install PNPM
17
- uses: pnpm/action-setup@v4
18
- with:
19
- run_install: false
20
-
21
- - name: Setup Node.js environment
22
- uses: actions/setup-node@v4
23
- with:
24
- node-version: 24
25
- cache: pnpm
26
-
27
- - name: Install deps
28
- run: pnpm install --frozen-lockfile
29
-
30
- - name: Check formatting
31
- run: pnpm run format:check
32
-
33
- - name: Check types
34
- run: pnpm run check-types
35
-
36
- - name: Lint
37
- run: pnpm run lint
38
-
39
- build:
40
- runs-on: ubuntu-latest
41
- steps:
42
- - name: Checkout
43
- uses: actions/checkout@v4
44
-
45
- - name: Install PNPM
46
- uses: pnpm/action-setup@v4
47
- with:
48
- run_install: false
49
-
50
- - name: Setup Node.js environment
51
- uses: actions/setup-node@v4
52
- with:
53
- node-version: 24
54
- cache: pnpm
55
-
56
- - name: Install deps
57
- run: pnpm install --frozen-lockfile
58
-
59
- - name: Build
60
- run: pnpm run build
61
-
62
- test:
63
- runs-on: ubuntu-latest
64
- strategy:
65
- matrix:
66
- node-version: [ 20, 22, 24 ]
67
-
68
- steps:
69
- - name: Checkout
70
- uses: actions/checkout@v4
71
-
72
- - name: Install PNPM
73
- uses: pnpm/action-setup@v4
74
- with:
75
- run_install: false
76
-
77
- - name: Use Node.js ${{ matrix.node-version }}
78
- uses: actions/setup-node@v4
79
- with:
80
- node-version: ${{ matrix.node-version }}
81
- cache: pnpm
82
-
83
- - name: Install deps
84
- run: pnpm install --frozen-lockfile
85
-
86
- - name: Run tests
87
- run: pnpm run test
package/.husky/commit-msg DELETED
@@ -1 +0,0 @@
1
- npx --no -- commitlint -e $1
package/.husky/pre-commit DELETED
@@ -1 +0,0 @@
1
- npx lint-staged
package/.megaignore DELETED
@@ -1,8 +0,0 @@
1
- +sync:.megaignore
2
- -d:node_modules
3
- -d:lib
4
- -f:yarn.lock
5
- -f:yalc.lock
6
- -f:package-lock.json
7
- -f:pnpm-lock.yaml
8
-
package/.prettierignore DELETED
@@ -1,3 +0,0 @@
1
- docs
2
- packages/*/lib
3
- lerna.json
@@ -1,8 +0,0 @@
1
- export default {
2
- extends: ['@stimulcross/commitlint-config'],
3
- rules: {
4
- 'body-max-line-length': [2, 'always', 500],
5
- 'footer-max-line-length': [2, 'always', 500],
6
- }
7
-
8
- };