@stimulcross/rate-limiter 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/lib/core/cancellable.d.ts +5 -0
- package/lib/core/cancellable.js +2 -0
- package/lib/core/clock.d.ts +10 -0
- package/lib/core/clock.js +2 -0
- package/{src/core/decision.ts → lib/core/decision.d.ts} +7 -11
- package/lib/core/decision.js +2 -0
- package/lib/core/rate-limit-policy.d.ts +14 -0
- package/lib/core/rate-limit-policy.js +2 -0
- package/lib/core/rate-limiter-status.d.ts +14 -0
- package/lib/core/rate-limiter-status.js +2 -0
- package/lib/core/rate-limiter.d.ts +34 -0
- package/lib/core/rate-limiter.js +2 -0
- package/lib/core/state-storage.d.ts +46 -0
- package/lib/core/state-storage.js +2 -0
- package/lib/enums/rate-limit-error-code.d.ts +26 -0
- package/lib/enums/rate-limit-error-code.js +27 -0
- package/lib/errors/custom.error.d.ts +6 -0
- package/lib/errors/custom.error.js +13 -0
- package/lib/errors/invalid-cost.error.d.ts +16 -0
- package/lib/errors/invalid-cost.error.js +26 -0
- package/lib/errors/rate-limit.error.d.ts +37 -0
- package/lib/errors/rate-limit.error.js +75 -0
- package/lib/errors/rate-limiter-destroyed.error.d.ts +7 -0
- package/lib/errors/rate-limiter-destroyed.error.js +9 -0
- package/{src/index.ts → lib/index.d.ts} +1 -0
- package/lib/index.js +5 -0
- package/lib/interfaces/rate-limiter-options.d.ts +76 -0
- package/lib/interfaces/rate-limiter-options.js +2 -0
- package/lib/interfaces/rate-limiter-queue-options.d.ts +42 -0
- package/lib/interfaces/rate-limiter-queue-options.js +2 -0
- package/lib/interfaces/rate-limiter-run-options.d.ts +52 -0
- package/lib/interfaces/rate-limiter-run-options.js +2 -0
- package/lib/limiters/abstract-rate-limiter.d.ts +44 -0
- package/lib/limiters/abstract-rate-limiter.js +133 -0
- package/lib/limiters/composite.policy.d.ts +15 -0
- package/lib/limiters/composite.policy.js +73 -0
- package/lib/limiters/fixed-window/fixed-window.limiter.d.ts +33 -0
- package/lib/limiters/fixed-window/fixed-window.limiter.js +85 -0
- package/lib/limiters/fixed-window/fixed-window.options.d.ts +27 -0
- package/lib/limiters/fixed-window/fixed-window.options.js +2 -0
- package/lib/limiters/fixed-window/fixed-window.policy.d.ts +19 -0
- package/lib/limiters/fixed-window/fixed-window.policy.js +121 -0
- package/{src/limiters/fixed-window/fixed-window.state.ts → lib/limiters/fixed-window/fixed-window.state.d.ts} +4 -3
- package/lib/limiters/fixed-window/fixed-window.state.js +2 -0
- package/lib/limiters/fixed-window/fixed-window.status.d.ts +39 -0
- package/lib/limiters/fixed-window/fixed-window.status.js +2 -0
- package/{src/limiters/fixed-window/index.ts → lib/limiters/fixed-window/index.d.ts} +1 -0
- package/lib/limiters/fixed-window/index.js +2 -0
- package/lib/limiters/generic-cell/generic-cell.limiter.d.ts +30 -0
- package/lib/limiters/generic-cell/generic-cell.limiter.js +74 -0
- package/lib/limiters/generic-cell/generic-cell.options.d.ts +22 -0
- package/lib/limiters/generic-cell/generic-cell.options.js +2 -0
- package/lib/limiters/generic-cell/generic-cell.policy.d.ts +18 -0
- package/lib/limiters/generic-cell/generic-cell.policy.js +87 -0
- package/{src/limiters/generic-cell/generic-cell.state.ts → lib/limiters/generic-cell/generic-cell.state.d.ts} +2 -1
- package/lib/limiters/generic-cell/generic-cell.state.js +2 -0
- package/lib/limiters/generic-cell/generic-cell.status.d.ts +49 -0
- package/lib/limiters/generic-cell/generic-cell.status.js +2 -0
- package/{src/limiters/generic-cell/index.ts → lib/limiters/generic-cell/index.d.ts} +1 -0
- package/lib/limiters/generic-cell/index.js +2 -0
- package/{src/limiters/http-response-based/http-limit-info.extractor.ts → lib/limiters/http-response-based/http-limit-info.extractor.d.ts} +2 -6
- package/lib/limiters/http-response-based/http-limit-info.extractor.js +2 -0
- package/lib/limiters/http-response-based/http-limit.info.d.ts +39 -0
- package/lib/limiters/http-response-based/http-limit.info.js +2 -0
- package/{src/limiters/http-response-based/http-response-based-limiter.options.ts → lib/limiters/http-response-based/http-response-based-limiter.options.d.ts} +9 -10
- package/lib/limiters/http-response-based/http-response-based-limiter.options.js +2 -0
- package/lib/limiters/http-response-based/http-response-based-limiter.state.d.ts +14 -0
- package/lib/limiters/http-response-based/http-response-based-limiter.state.js +2 -0
- package/lib/limiters/http-response-based/http-response-based-limiter.status.d.ts +70 -0
- package/lib/limiters/http-response-based/http-response-based-limiter.status.js +2 -0
- package/lib/limiters/http-response-based/http-response-based.limiter.d.ts +56 -0
- package/lib/limiters/http-response-based/http-response-based.limiter.js +386 -0
- package/{src/limiters/http-response-based/index.ts → lib/limiters/http-response-based/index.d.ts} +1 -0
- package/lib/limiters/http-response-based/index.js +2 -0
- package/{src/limiters/leaky-bucket/index.ts → lib/limiters/leaky-bucket/index.d.ts} +1 -0
- package/lib/limiters/leaky-bucket/index.js +2 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.limiter.d.ts +30 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.limiter.js +75 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.options.d.ts +22 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.options.js +2 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.policy.d.ts +19 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.policy.js +101 -0
- package/{src/limiters/leaky-bucket/leaky-bucket.state.ts → lib/limiters/leaky-bucket/leaky-bucket.state.d.ts} +3 -2
- package/lib/limiters/leaky-bucket/leaky-bucket.state.js +2 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.status.d.ts +31 -0
- package/lib/limiters/leaky-bucket/leaky-bucket.status.js +2 -0
- package/{src/limiters/sliding-window-counter/index.ts → lib/limiters/sliding-window-counter/index.d.ts} +2 -4
- package/lib/limiters/sliding-window-counter/index.js +2 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.limiter.d.ts +28 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.limiter.js +47 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.options.d.ts +16 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.options.js +2 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.policy.d.ts +18 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.policy.js +128 -0
- package/{src/limiters/sliding-window-counter/sliding-window-counter.state.ts → lib/limiters/sliding-window-counter/sliding-window-counter.state.d.ts} +4 -3
- package/lib/limiters/sliding-window-counter/sliding-window-counter.state.js +2 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.status.d.ts +45 -0
- package/lib/limiters/sliding-window-counter/sliding-window-counter.status.js +2 -0
- package/{src/limiters/sliding-window-log/index.ts → lib/limiters/sliding-window-log/index.d.ts} +1 -0
- package/lib/limiters/sliding-window-log/index.js +2 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.limiter.d.ts +27 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.limiter.js +44 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.options.d.ts +16 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.options.js +2 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.policy.d.ts +18 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.policy.js +124 -0
- package/{src/limiters/sliding-window-log/sliding-window-log.state.ts → lib/limiters/sliding-window-log/sliding-window-log.state.d.ts} +5 -6
- package/lib/limiters/sliding-window-log/sliding-window-log.state.js +2 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.status.d.ts +39 -0
- package/lib/limiters/sliding-window-log/sliding-window-log.status.js +2 -0
- package/{src/limiters/token-bucket/index.ts → lib/limiters/token-bucket/index.d.ts} +1 -0
- package/lib/limiters/token-bucket/index.js +2 -0
- package/lib/limiters/token-bucket/token-bucket.limiter.d.ts +30 -0
- package/lib/limiters/token-bucket/token-bucket.limiter.js +75 -0
- package/{src/limiters/token-bucket/token-bucket.options.ts → lib/limiters/token-bucket/token-bucket.options.d.ts} +9 -10
- package/lib/limiters/token-bucket/token-bucket.options.js +2 -0
- package/lib/limiters/token-bucket/token-bucket.policy.d.ts +19 -0
- package/lib/limiters/token-bucket/token-bucket.policy.js +116 -0
- package/{src/limiters/token-bucket/token-bucket.state.ts → lib/limiters/token-bucket/token-bucket.state.d.ts} +4 -3
- package/lib/limiters/token-bucket/token-bucket.state.js +2 -0
- package/lib/limiters/token-bucket/token-bucket.status.d.ts +31 -0
- package/lib/limiters/token-bucket/token-bucket.status.js +2 -0
- package/lib/runtime/default-clock.d.ts +4 -0
- package/lib/runtime/default-clock.js +7 -0
- package/lib/runtime/execution-tickets.d.ts +12 -0
- package/lib/runtime/execution-tickets.js +27 -0
- package/lib/runtime/in-memory-state-store.d.ts +19 -0
- package/lib/runtime/in-memory-state-store.js +97 -0
- package/lib/runtime/rate-limiter.executor.d.ts +47 -0
- package/lib/runtime/rate-limiter.executor.js +196 -0
- package/lib/runtime/semaphore.d.ts +9 -0
- package/lib/runtime/semaphore.js +28 -0
- package/lib/runtime/task.d.ts +41 -0
- package/lib/runtime/task.js +101 -0
- package/{src/types/limit-behavior.ts → lib/types/limit-behavior.d.ts} +1 -0
- package/lib/types/limit-behavior.js +2 -0
- package/lib/utils/generate-random-string.d.ts +3 -0
- package/lib/utils/generate-random-string.js +13 -0
- package/lib/utils/promise-with-resolvers.d.ts +9 -0
- package/lib/utils/promise-with-resolvers.js +15 -0
- package/lib/utils/sanitize-error.d.ts +3 -0
- package/lib/utils/sanitize-error.js +5 -0
- package/lib/utils/sanitize-priority.d.ts +4 -0
- package/lib/utils/sanitize-priority.js +18 -0
- package/lib/utils/validate-cost.d.ts +3 -0
- package/lib/utils/validate-cost.js +14 -0
- package/package.json +13 -2
- package/.editorconfig +0 -21
- package/.github/workflows/node.yml +0 -87
- package/.husky/commit-msg +0 -1
- package/.husky/pre-commit +0 -1
- package/.megaignore +0 -8
- package/.prettierignore +0 -3
- package/commitlint.config.js +0 -8
- package/eslint.config.js +0 -65
- package/lint-staged.config.js +0 -4
- package/prettier.config.cjs +0 -1
- package/src/core/cancellable.ts +0 -4
- package/src/core/clock.ts +0 -9
- package/src/core/rate-limit-policy.ts +0 -15
- package/src/core/rate-limiter-status.ts +0 -14
- package/src/core/rate-limiter.ts +0 -37
- package/src/core/state-storage.ts +0 -51
- package/src/enums/rate-limit-error-code.ts +0 -29
- package/src/errors/custom.error.ts +0 -14
- package/src/errors/invalid-cost.error.ts +0 -33
- package/src/errors/rate-limit.error.ts +0 -91
- package/src/errors/rate-limiter-destroyed.error.ts +0 -8
- package/src/interfaces/rate-limiter-options.ts +0 -84
- package/src/interfaces/rate-limiter-queue-options.ts +0 -45
- package/src/interfaces/rate-limiter-run-options.ts +0 -58
- package/src/limiters/abstract-rate-limiter.ts +0 -206
- package/src/limiters/composite.policy.ts +0 -102
- package/src/limiters/fixed-window/fixed-window.limiter.ts +0 -121
- package/src/limiters/fixed-window/fixed-window.options.ts +0 -29
- package/src/limiters/fixed-window/fixed-window.policy.ts +0 -159
- package/src/limiters/fixed-window/fixed-window.status.ts +0 -46
- package/src/limiters/generic-cell/generic-cell.limiter.ts +0 -108
- package/src/limiters/generic-cell/generic-cell.options.ts +0 -23
- package/src/limiters/generic-cell/generic-cell.policy.ts +0 -115
- package/src/limiters/generic-cell/generic-cell.status.ts +0 -54
- package/src/limiters/http-response-based/http-limit.info.ts +0 -41
- package/src/limiters/http-response-based/http-response-based-limiter.state.ts +0 -13
- package/src/limiters/http-response-based/http-response-based-limiter.status.ts +0 -74
- package/src/limiters/http-response-based/http-response-based.limiter.ts +0 -512
- package/src/limiters/leaky-bucket/leaky-bucket.limiter.ts +0 -105
- package/src/limiters/leaky-bucket/leaky-bucket.options.ts +0 -23
- package/src/limiters/leaky-bucket/leaky-bucket.policy.ts +0 -134
- package/src/limiters/leaky-bucket/leaky-bucket.status.ts +0 -36
- package/src/limiters/sliding-window-counter/sliding-window-counter.limiter.ts +0 -76
- package/src/limiters/sliding-window-counter/sliding-window-counter.options.ts +0 -20
- package/src/limiters/sliding-window-counter/sliding-window-counter.policy.ts +0 -167
- package/src/limiters/sliding-window-counter/sliding-window-counter.status.ts +0 -53
- package/src/limiters/sliding-window-log/sliding-window-log.limiter.ts +0 -65
- package/src/limiters/sliding-window-log/sliding-window-log.options.ts +0 -20
- package/src/limiters/sliding-window-log/sliding-window-log.policy.ts +0 -166
- package/src/limiters/sliding-window-log/sliding-window-log.status.ts +0 -44
- package/src/limiters/token-bucket/token-bucket.limiter.ts +0 -110
- package/src/limiters/token-bucket/token-bucket.policy.ts +0 -155
- package/src/limiters/token-bucket/token-bucket.status.ts +0 -36
- package/src/runtime/default-clock.ts +0 -8
- package/src/runtime/execution-tickets.ts +0 -34
- package/src/runtime/in-memory-state-store.ts +0 -135
- package/src/runtime/rate-limiter.executor.ts +0 -286
- package/src/runtime/semaphore.ts +0 -31
- package/src/runtime/task.ts +0 -141
- package/src/utils/generate-random-string.ts +0 -16
- package/src/utils/promise-with-resolvers.ts +0 -23
- package/src/utils/sanitize-error.ts +0 -4
- package/src/utils/sanitize-priority.ts +0 -22
- package/src/utils/validate-cost.ts +0 -16
- package/tests/integration/limiters/fixed-window.limiter.spec.ts +0 -371
- package/tests/integration/limiters/generic-cell.limiter.spec.ts +0 -361
- package/tests/integration/limiters/http-response-based.limiter.spec.ts +0 -833
- package/tests/integration/limiters/leaky-bucket.spec.ts +0 -357
- package/tests/integration/limiters/sliding-window-counter.limiter.spec.ts +0 -175
- package/tests/integration/limiters/sliding-window-log.spec.ts +0 -185
- package/tests/integration/limiters/token-bucket.limiter.spec.ts +0 -363
- package/tests/tsconfig.json +0 -4
- package/tests/unit/policies/composite.policy.spec.ts +0 -244
- package/tests/unit/policies/fixed-window.policy.spec.ts +0 -260
- package/tests/unit/policies/generic-cell.policy.spec.ts +0 -178
- package/tests/unit/policies/leaky-bucket.policy.spec.ts +0 -215
- package/tests/unit/policies/sliding-window-counter.policy.spec.ts +0 -209
- package/tests/unit/policies/sliding-window-log.policy.spec.ts +0 -285
- package/tests/unit/policies/token-bucket.policy.spec.ts +0 -371
- package/tests/unit/runtime/execution-tickets.spec.ts +0 -121
- package/tests/unit/runtime/in-memory-state-store.spec.ts +0 -238
- package/tests/unit/runtime/rate-limiter.executor.spec.ts +0 -353
- package/tests/unit/runtime/semaphore.spec.ts +0 -98
- package/tests/unit/runtime/task.spec.ts +0 -182
- package/tests/unit/utils/generate-random-string.spec.ts +0 -51
- package/tests/unit/utils/promise-with-resolvers.spec.ts +0 -57
- package/tests/unit/utils/sanitize-priority.spec.ts +0 -46
- package/tests/unit/utils/validate-cost.spec.ts +0 -48
- package/tsconfig.json +0 -14
- package/vitest.config.js +0 -22
|
@@ -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,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
|
|
@@ -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,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,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.
|
|
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
|
-
"
|
|
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
package/.prettierignore
DELETED