@stimulcross/rate-limiter 0.0.1

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 (112) hide show
  1. package/.editorconfig +21 -0
  2. package/.github/workflows/node.yml +87 -0
  3. package/.husky/commit-msg +1 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.megaignore +8 -0
  6. package/.prettierignore +3 -0
  7. package/LICENSE +21 -0
  8. package/README.md +7 -0
  9. package/commitlint.config.js +8 -0
  10. package/eslint.config.js +65 -0
  11. package/lint-staged.config.js +4 -0
  12. package/package.json +89 -0
  13. package/prettier.config.cjs +1 -0
  14. package/src/core/cancellable.ts +4 -0
  15. package/src/core/clock.ts +9 -0
  16. package/src/core/decision.ts +27 -0
  17. package/src/core/rate-limit-policy.ts +15 -0
  18. package/src/core/rate-limiter-status.ts +14 -0
  19. package/src/core/rate-limiter.ts +37 -0
  20. package/src/core/state-storage.ts +51 -0
  21. package/src/enums/rate-limit-error-code.ts +29 -0
  22. package/src/errors/custom.error.ts +14 -0
  23. package/src/errors/invalid-cost.error.ts +33 -0
  24. package/src/errors/rate-limit.error.ts +91 -0
  25. package/src/errors/rate-limiter-destroyed.error.ts +8 -0
  26. package/src/index.ts +11 -0
  27. package/src/interfaces/rate-limiter-options.ts +84 -0
  28. package/src/interfaces/rate-limiter-queue-options.ts +45 -0
  29. package/src/interfaces/rate-limiter-run-options.ts +58 -0
  30. package/src/limiters/abstract-rate-limiter.ts +206 -0
  31. package/src/limiters/composite.policy.ts +102 -0
  32. package/src/limiters/fixed-window/fixed-window.limiter.ts +121 -0
  33. package/src/limiters/fixed-window/fixed-window.options.ts +29 -0
  34. package/src/limiters/fixed-window/fixed-window.policy.ts +159 -0
  35. package/src/limiters/fixed-window/fixed-window.state.ts +10 -0
  36. package/src/limiters/fixed-window/fixed-window.status.ts +46 -0
  37. package/src/limiters/fixed-window/index.ts +4 -0
  38. package/src/limiters/generic-cell/generic-cell.limiter.ts +108 -0
  39. package/src/limiters/generic-cell/generic-cell.options.ts +23 -0
  40. package/src/limiters/generic-cell/generic-cell.policy.ts +115 -0
  41. package/src/limiters/generic-cell/generic-cell.state.ts +8 -0
  42. package/src/limiters/generic-cell/generic-cell.status.ts +54 -0
  43. package/src/limiters/generic-cell/index.ts +4 -0
  44. package/src/limiters/http-response-based/http-limit-info.extractor.ts +20 -0
  45. package/src/limiters/http-response-based/http-limit.info.ts +41 -0
  46. package/src/limiters/http-response-based/http-response-based-limiter.options.ts +18 -0
  47. package/src/limiters/http-response-based/http-response-based-limiter.state.ts +13 -0
  48. package/src/limiters/http-response-based/http-response-based-limiter.status.ts +74 -0
  49. package/src/limiters/http-response-based/http-response-based.limiter.ts +512 -0
  50. package/src/limiters/http-response-based/index.ts +6 -0
  51. package/src/limiters/leaky-bucket/index.ts +4 -0
  52. package/src/limiters/leaky-bucket/leaky-bucket.limiter.ts +105 -0
  53. package/src/limiters/leaky-bucket/leaky-bucket.options.ts +23 -0
  54. package/src/limiters/leaky-bucket/leaky-bucket.policy.ts +134 -0
  55. package/src/limiters/leaky-bucket/leaky-bucket.state.ts +9 -0
  56. package/src/limiters/leaky-bucket/leaky-bucket.status.ts +36 -0
  57. package/src/limiters/sliding-window-counter/index.ts +7 -0
  58. package/src/limiters/sliding-window-counter/sliding-window-counter.limiter.ts +76 -0
  59. package/src/limiters/sliding-window-counter/sliding-window-counter.options.ts +20 -0
  60. package/src/limiters/sliding-window-counter/sliding-window-counter.policy.ts +167 -0
  61. package/src/limiters/sliding-window-counter/sliding-window-counter.state.ts +10 -0
  62. package/src/limiters/sliding-window-counter/sliding-window-counter.status.ts +53 -0
  63. package/src/limiters/sliding-window-log/index.ts +4 -0
  64. package/src/limiters/sliding-window-log/sliding-window-log.limiter.ts +65 -0
  65. package/src/limiters/sliding-window-log/sliding-window-log.options.ts +20 -0
  66. package/src/limiters/sliding-window-log/sliding-window-log.policy.ts +166 -0
  67. package/src/limiters/sliding-window-log/sliding-window-log.state.ts +19 -0
  68. package/src/limiters/sliding-window-log/sliding-window-log.status.ts +44 -0
  69. package/src/limiters/token-bucket/index.ts +4 -0
  70. package/src/limiters/token-bucket/token-bucket.limiter.ts +110 -0
  71. package/src/limiters/token-bucket/token-bucket.options.ts +17 -0
  72. package/src/limiters/token-bucket/token-bucket.policy.ts +155 -0
  73. package/src/limiters/token-bucket/token-bucket.state.ts +10 -0
  74. package/src/limiters/token-bucket/token-bucket.status.ts +36 -0
  75. package/src/runtime/default-clock.ts +8 -0
  76. package/src/runtime/execution-tickets.ts +34 -0
  77. package/src/runtime/in-memory-state-store.ts +135 -0
  78. package/src/runtime/rate-limiter.executor.ts +286 -0
  79. package/src/runtime/semaphore.ts +31 -0
  80. package/src/runtime/task.ts +141 -0
  81. package/src/types/limit-behavior.ts +8 -0
  82. package/src/utils/generate-random-string.ts +16 -0
  83. package/src/utils/promise-with-resolvers.ts +23 -0
  84. package/src/utils/sanitize-error.ts +4 -0
  85. package/src/utils/sanitize-priority.ts +22 -0
  86. package/src/utils/validate-cost.ts +16 -0
  87. package/tests/integration/limiters/fixed-window.limiter.spec.ts +371 -0
  88. package/tests/integration/limiters/generic-cell.limiter.spec.ts +361 -0
  89. package/tests/integration/limiters/http-response-based.limiter.spec.ts +833 -0
  90. package/tests/integration/limiters/leaky-bucket.spec.ts +357 -0
  91. package/tests/integration/limiters/sliding-window-counter.limiter.spec.ts +175 -0
  92. package/tests/integration/limiters/sliding-window-log.spec.ts +185 -0
  93. package/tests/integration/limiters/token-bucket.limiter.spec.ts +363 -0
  94. package/tests/tsconfig.json +4 -0
  95. package/tests/unit/policies/composite.policy.spec.ts +244 -0
  96. package/tests/unit/policies/fixed-window.policy.spec.ts +260 -0
  97. package/tests/unit/policies/generic-cell.policy.spec.ts +178 -0
  98. package/tests/unit/policies/leaky-bucket.policy.spec.ts +215 -0
  99. package/tests/unit/policies/sliding-window-counter.policy.spec.ts +209 -0
  100. package/tests/unit/policies/sliding-window-log.policy.spec.ts +285 -0
  101. package/tests/unit/policies/token-bucket.policy.spec.ts +371 -0
  102. package/tests/unit/runtime/execution-tickets.spec.ts +121 -0
  103. package/tests/unit/runtime/in-memory-state-store.spec.ts +238 -0
  104. package/tests/unit/runtime/rate-limiter.executor.spec.ts +353 -0
  105. package/tests/unit/runtime/semaphore.spec.ts +98 -0
  106. package/tests/unit/runtime/task.spec.ts +182 -0
  107. package/tests/unit/utils/generate-random-string.spec.ts +51 -0
  108. package/tests/unit/utils/promise-with-resolvers.spec.ts +57 -0
  109. package/tests/unit/utils/sanitize-priority.spec.ts +46 -0
  110. package/tests/unit/utils/validate-cost.spec.ts +48 -0
  111. package/tsconfig.json +14 -0
  112. package/vitest.config.js +22 -0
package/.editorconfig ADDED
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,87 @@
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
@@ -0,0 +1 @@
1
+ npx --no -- commitlint -e $1
@@ -0,0 +1 @@
1
+ npx lint-staged
package/.megaignore ADDED
@@ -0,0 +1,8 @@
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
+
@@ -0,0 +1,3 @@
1
+ docs
2
+ packages/*/lib
3
+ lerna.json
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stimul Cross
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # Rate Limiters
2
+
3
+ A collection of rate limiters designed primarily for **outbound request throttling**.
4
+
5
+ They are suited for client-side usage to respect third-party limits or protect internal resources.
6
+
7
+ While it is technically possible to use these limiters for server-side traffic backed by a distributed store like Redis, it is **not recommended**. The algorithms evaluate state within the application process, so distributed usage requires multiple network operations per request introducing significant round-trip latency.
@@ -0,0 +1,8 @@
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
+ };
@@ -0,0 +1,65 @@
1
+ import { resolveFlatConfig } from '@leancodepl/resolve-eslint-flat-config';
2
+ import typescript from '@stimulcross/eslint-config-typescript';
3
+ import typescriptStyle from '@stimulcross/eslint-config-typescript/style';
4
+ import { defineConfig, globalIgnores } from 'eslint/config';
5
+ import globals from 'globals';
6
+
7
+ export const globs = {
8
+ js: ['**/*.js', '**/*.cjs', '**/*.mjs'],
9
+ ts: ['**/*.ts', '**/*.cts', '**/*.mts'],
10
+ jsSpec: ['**/*.spec.js', '**/*.spec.cjs', '**/*.spec.mjs'],
11
+ tsSpec: ['**/*.spec.ts', '**/*.spec.cts', '**/*.spec.mts'],
12
+ lib: '**/dist',
13
+ nodeModules: '**/node_modules',
14
+ coverage: '**/coverage',
15
+ dts: '**/*.d.ts',
16
+ };
17
+
18
+ /** @type {import("eslint").Linter.Config[]} */
19
+ export const config = resolveFlatConfig(
20
+ defineConfig(
21
+ globalIgnores([globs.lib, globs.nodeModules, globs.dts, globs.coverage]),
22
+ {
23
+ files: [...globs.js, ...globs.ts, ...globs.jsSpec, ...globs.tsSpec],
24
+ languageOptions: {
25
+ globals: {
26
+ ...globals.node,
27
+ ...globals.es2022,
28
+ },
29
+ },
30
+ },
31
+ {
32
+ files: [...globs.js, ...globs.ts, ...globs.tsSpec],
33
+ extends: [typescript, typescriptStyle],
34
+ },
35
+ {
36
+ files: [...globs.ts, ...globs.tsSpec],
37
+ rules: {
38
+ 'id-length': 'off',
39
+ 'no-await-in-loop': 'off',
40
+ 'unicorn/no-new-array': 'off',
41
+ 'unicorn/no-thenable': 'off',
42
+ 'unicorn/no-useless-undefined': 'error',
43
+ '@typescript-eslint/no-explicit-any': 'off',
44
+ '@typescript-eslint/no-non-null-assertion': 'off',
45
+ '@typescript-eslint/no-unnecessary-condition': ['warn', { allowConstantLoopConditions: true }],
46
+ },
47
+ },
48
+ {
49
+ files: [...globs.jsSpec, ...globs.tsSpec],
50
+ rules: {
51
+ 'id-length': 'off',
52
+ 'max-nested-callbacks': 'off',
53
+ 'unicorn/consistent-function-scoping': 'off',
54
+ 'unicorn/no-array-push-push': 'off',
55
+ '@typescript-eslint/naming-convention': 'off',
56
+ '@typescript-eslint/no-empty-function': 'off',
57
+ '@typescript-eslint/no-unsafe-member-access': 'off',
58
+ '@typescript-eslint/no-unsafe-return': 'off',
59
+ '@typescript-eslint/unbound-method': 'off',
60
+ },
61
+ },
62
+ ),
63
+ );
64
+
65
+ export default config;
@@ -0,0 +1,4 @@
1
+ export default {
2
+ '*.{js,mjs,ts,json,md}': 'prettier --write ',
3
+ 'src/*.{js,ts}': 'eslint src',
4
+ };
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@stimulcross/rate-limiter",
3
+ "version": "0.0.1",
4
+ "description": "A collection of client-side rate limiters for Node.js and browsers.",
5
+ "engines": {
6
+ "node": ">=20"
7
+ },
8
+ "type": "module",
9
+ "main": "./lib/index.js",
10
+ "types": "./lib/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./lib/index.d.ts",
14
+ "default": "./lib/index.js"
15
+ },
16
+ "./fixed-window": {
17
+ "types": "./lib/limiters/fixed-window/index.d.ts",
18
+ "default": "./lib/limiters/fixed-window/index.js"
19
+ },
20
+ "./sliding-window-counter": {
21
+ "types": "./lib/limiters/sliding-window-counter/index.d.ts",
22
+ "default": "./lib/limiters/sliding-window-counter/index.js"
23
+ },
24
+ "./sliding-window-log": {
25
+ "types": "./lib/limiters/sliding-window-log/index.d.ts",
26
+ "default": "./lib/limiters/sliding-window-log/index.js"
27
+ },
28
+ "./token-bucket": {
29
+ "types": "./lib/limiters/token-bucket/index.d.ts",
30
+ "default": "./lib/limiters/token-bucket/index.js"
31
+ },
32
+ "./leaky-bucket": {
33
+ "types": "./lib/limiters/leaky-bucket/index.d.ts",
34
+ "default": "./lib/limiters/leaky-bucket/index.js"
35
+ },
36
+ "./generic-cell": {
37
+ "types": "./lib/limiters/generic-cell/index.d.ts",
38
+ "default": "./lib/limiters/generic-cell/index.js"
39
+ },
40
+ "./http-response-based": {
41
+ "types": "./lib/limiters/http-response-based/index.d.ts",
42
+ "default": "./lib/limiters/http-response-based/index.js"
43
+ }
44
+ },
45
+ "dependencies": {
46
+ "@stimulcross/ds-binary-heap": "^0.1.0",
47
+ "@stimulcross/ds-deque": "^0.1.0",
48
+ "@stimulcross/ds-policy-priority-queue": "^0.2.2",
49
+ "@stimulcross/logger": "^8.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "@commitlint/cli": "^20.4.4",
53
+ "@leancodepl/resolve-eslint-flat-config": "^9.7.4",
54
+ "@stimulcross/commitlint-config": "^2.0.0",
55
+ "@stimulcross/eslint-config-node": "^2.0.0",
56
+ "@stimulcross/eslint-config-typescript": "^2.0.0",
57
+ "@stimulcross/prettier-config": "^2.0.0",
58
+ "@types/node": "^25.5.0",
59
+ "@vitest/coverage-v8": "^4.1.0",
60
+ "del-cli": "^7.0.0",
61
+ "eslint": "^9.39.4",
62
+ "globals": "^17.4.0",
63
+ "husky": "^9.1.7",
64
+ "lint-staged": "^16.4.0",
65
+ "prettier": "^3.8.1",
66
+ "typescript": "^5.9.3",
67
+ "vitest": "^4.1.0"
68
+ },
69
+ "publishConfig": {
70
+ "access": "public"
71
+ },
72
+ "scripts": {
73
+ "build": "tsc -p tsconfig.json",
74
+ "rebuild": "pnpm run clean && pnpm run build",
75
+ "clean": "del-cli lib *.tsbuildinfo",
76
+ "lint": "eslint src tests",
77
+ "lint:fix": "eslint src tests --fix",
78
+ "format:check": "prettier --config \"prettier.config.cjs\" --check src tests",
79
+ "format:fix": "prettier --config \"prettier.config.cjs\" --write src tests",
80
+ "check-types": "tsc -p tsconfig.json --noEmit",
81
+ "preversion": "pnpm run format:check && pnpm run lint && pnpm run test && pnpm run rebuild",
82
+ "postversion": "pnpm i",
83
+ "test": "vitest run",
84
+ "test:cov": "vitest run --coverage",
85
+ "test:watch": "vitest watch",
86
+ "test:verbose": "vitest --run --reporter verbose --logHeapUsage",
87
+ "test:watch:cov": "vitest watch --coverage"
88
+ }
89
+ }
@@ -0,0 +1 @@
1
+ module.exports = require('@stimulcross/prettier-config');
@@ -0,0 +1,4 @@
1
+ /** @internal */
2
+ export interface Cancellable {
3
+ cancel(): void;
4
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Clock interface.
3
+ */
4
+ export interface Clock {
5
+ /**
6
+ * Returns the current timestamp in milliseconds.
7
+ */
8
+ now(): number;
9
+ }
@@ -0,0 +1,27 @@
1
+ /** @internal */
2
+ export type DecisionKind = 'allow' | 'deny' | 'delay';
3
+
4
+ /** @internal */
5
+ export interface DecisionBase {
6
+ kind: DecisionKind;
7
+ }
8
+
9
+ /** @internal */
10
+ export interface DecisionAllow extends DecisionBase {
11
+ kind: Extract<DecisionKind, 'allow'>;
12
+ }
13
+
14
+ /** @internal */
15
+ export interface DecisionDeny extends DecisionBase {
16
+ kind: Extract<DecisionKind, 'deny'>;
17
+ retryAt: number;
18
+ }
19
+
20
+ /** @internal */
21
+ export interface DecisionDelay extends DecisionBase {
22
+ kind: Extract<DecisionKind, 'delay'>;
23
+ runAt: number;
24
+ }
25
+
26
+ /** @internal */
27
+ export type Decision = DecisionAllow | DecisionDeny | DecisionDelay;
@@ -0,0 +1,15 @@
1
+ import { type Decision } from './decision.js';
2
+
3
+ /** @internal */
4
+ export interface RateLimitPolicyResult<S> {
5
+ decision: Decision;
6
+ nextState: S;
7
+ }
8
+
9
+ /** @internal */
10
+ export interface RateLimitPolicy<TState extends object = object, TStatus extends object = object> {
11
+ getInitialState(now: number): TState;
12
+ getStatus(state: TState, now: number): TStatus;
13
+ evaluate(state: TState, now: number, cost: number, shouldReserve?: boolean): RateLimitPolicyResult<TState>;
14
+ revert(state: TState, cost: number, now: number): TState;
15
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The status of the rate limiter.
3
+ */
4
+ export interface RateLimiterStatus {
5
+ /**
6
+ * The timestamp (in milliseconds) when a rate limiter will allow a single request.
7
+ */
8
+ readonly nextAvailableAt: number;
9
+
10
+ /**
11
+ * The timestamp (in milliseconds) when the rate limiter will reset.
12
+ */
13
+ readonly resetAt: number;
14
+ }
@@ -0,0 +1,37 @@
1
+ import { type RateLimiterRunOptions } from '../interfaces/rate-limiter-run-options.js';
2
+
3
+ /**
4
+ * Rate limiter interface.
5
+ *
6
+ * @template TStatus The type of the rate limiter status returned by {@link getStatus} method.
7
+ */
8
+ export interface RateLimiter<TStatus extends object = object> {
9
+ /**
10
+ * Runs the given task.
11
+ *
12
+ * @param task The task to run.
13
+ * @param options Options for running the task.
14
+ */
15
+ run<T>(task: () => T | Promise<T>, options?: RateLimiterRunOptions): Promise<T>;
16
+
17
+ /**
18
+ * Clears the rate limiter state.
19
+ *
20
+ * @param key The optional key to clear the state for.
21
+ */
22
+ clear(key?: string): Promise<void>;
23
+
24
+ /**
25
+ * Gets the rate limiter's status.
26
+ *
27
+ * @param key The optional key to get the status for.
28
+ */
29
+ getStatus?(key?: string): Promise<TStatus>;
30
+
31
+ /**
32
+ * Destroys the rate limiter.
33
+ *
34
+ * The limiter cannot be used after it has been destroyed. It should be used only for graceful shutdown.
35
+ */
36
+ destroy?(): Promise<void>;
37
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * State storage interface.
3
+ */
4
+ export interface StateStorage<TState> {
5
+ /**
6
+ * Gets the state for the given key.
7
+ *
8
+ * @param key The key to get the state for.
9
+ */
10
+ get(key: string): Promise<TState | null>;
11
+
12
+ /**
13
+ * Sets the state for the given key.
14
+ *
15
+ * @param key The key to set the state for.
16
+ * @param value The state to set.
17
+ * @param ttlMs Optional TTL in milliseconds.
18
+ */
19
+ set(key: string, value: TState, ttlMs?: number): Promise<void>;
20
+
21
+ /**
22
+ * Deletes the state for the given key.
23
+ *
24
+ * @param key The key to delete the state for.
25
+ */
26
+ delete(key: string): Promise<void>;
27
+
28
+ /**
29
+ * Clears all stored states.
30
+ */
31
+ clear(): Promise<void>;
32
+
33
+ /**
34
+ * Destroys the storage.
35
+ */
36
+ destroy?(): Promise<void>;
37
+
38
+ /**
39
+ * Acquires a lock for the given key.
40
+ *
41
+ * @param key The key to acquire the lock for.
42
+ */
43
+ acquireLock?(key: string): Promise<void>;
44
+
45
+ /**
46
+ * Releases the lock for the given key.
47
+ *
48
+ * @param key The key to release the lock for.
49
+ */
50
+ releaseLock?(key: string): Promise<void>;
51
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Rate limiter error codes.
3
+ */
4
+ export enum RateLimitErrorCode {
5
+ /**
6
+ * Indicates that the limit has been reached.
7
+ */
8
+ LimitExceeded = 'LIMIT_EXCEEDED',
9
+
10
+ /**
11
+ * Indicates that the execution queue is full.
12
+ */
13
+ QueueOverflow = 'QUEUE_OVERFLOW',
14
+
15
+ /**
16
+ * Indicates that the task has expired.
17
+ */
18
+ Expired = 'EXPIRED',
19
+
20
+ /**
21
+ * Indicates that a task was cleared before it was executed.
22
+ */
23
+ Destroyed = 'DESTROYED',
24
+
25
+ /**
26
+ * Indicates that a task was canceled via abort controller before it was executed.
27
+ */
28
+ Cancelled = 'CANCELLED',
29
+ }
@@ -0,0 +1,14 @@
1
+ /** @internal */
2
+ export abstract class CustomError extends Error {
3
+ protected constructor(message: string) {
4
+ super(message);
5
+
6
+ Object.setPrototypeOf(this, new.target.prototype);
7
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
8
+ Error.captureStackTrace?.(this, new.target.constructor);
9
+ }
10
+
11
+ public get name(): string {
12
+ return this.constructor.name;
13
+ }
14
+ }
@@ -0,0 +1,33 @@
1
+ import { CustomError } from './custom.error.js';
2
+
3
+ export interface InvalidCostErrorPlainObject extends Error {
4
+ cost: number;
5
+ }
6
+
7
+ /**
8
+ * Error thrown when the cost is invalid.
9
+ *
10
+ * The cost must be a positive integer or zero.
11
+ */
12
+ export class InvalidCostError extends CustomError {
13
+ constructor(
14
+ message: string,
15
+ private readonly _cost: number,
16
+ ) {
17
+ super(message);
18
+ }
19
+
20
+ public get cost(): number {
21
+ return this._cost;
22
+ }
23
+
24
+ // eslint-disable-next-line @typescript-eslint/naming-convention
25
+ public toJSON(): InvalidCostErrorPlainObject {
26
+ return {
27
+ name: this.name,
28
+ message: this.message,
29
+ cost: this._cost,
30
+ stack: this.stack,
31
+ };
32
+ }
33
+ }
@@ -0,0 +1,91 @@
1
+ import { RateLimitErrorCode } from '../enums/rate-limit-error-code.js';
2
+
3
+ export interface RateLimitErrorPlainObject extends Error {
4
+ code: RateLimitErrorCode;
5
+ retryAt: number | null;
6
+ }
7
+
8
+ /**
9
+ * An error thrown when a rate limit is exceeded.
10
+ *
11
+ * This error has a {@link code} property that indicates the type of error.
12
+ *
13
+ * The `code` can be:
14
+ * - `LIMIT_EXCEEDED` - When the rate limit is exceeded.
15
+ * - `QUEUE_OVERFLOW` - When the queue is full (if the limiter has a queue and the capacity has been exceeded).
16
+ * - `EXPIRED` - When the task has expired (waited too long in the queue). This is related to the `maxWaitMs` option.
17
+ * **NOTE:** This is never thrown if the task is executing too long. Such scenarios should be handled by the
18
+ * task itself.
19
+ * - `DESTROYED` - When the task is destroyed due to the rate limiter's `clear()` or `destroy()` methods.
20
+ * - `CANCELLED` - When the task is cancelled using an abort signal.
21
+ */
22
+ export class RateLimitError extends Error {
23
+ private readonly _code: RateLimitErrorCode;
24
+ private readonly _retryAt: number | null;
25
+
26
+ /** @internal */
27
+ constructor(code: RateLimitErrorCode, retryAt?: number, message?: string) {
28
+ if (!message) {
29
+ switch (code) {
30
+ case RateLimitErrorCode.LimitExceeded: {
31
+ message = `Rate limit exceeded.${retryAt ? ` Retry at ${new Date(retryAt).toISOString()}.` : ''}`;
32
+ break;
33
+ }
34
+
35
+ case RateLimitErrorCode.QueueOverflow: {
36
+ message = 'Queue overflow.';
37
+ break;
38
+ }
39
+
40
+ case RateLimitErrorCode.Expired: {
41
+ message = 'Task expired.';
42
+ break;
43
+ }
44
+
45
+ case RateLimitErrorCode.Destroyed: {
46
+ message = 'Task destroyed.';
47
+ break;
48
+ }
49
+
50
+ case RateLimitErrorCode.Cancelled: {
51
+ message = 'Task cancelled.';
52
+ break;
53
+ }
54
+
55
+ // No default
56
+ }
57
+ }
58
+
59
+ super(message);
60
+
61
+ this._code = code;
62
+ this._retryAt = retryAt ?? null;
63
+ }
64
+
65
+ /**
66
+ * The error code.
67
+ */
68
+ public get code(): RateLimitErrorCode {
69
+ return this._code;
70
+ }
71
+
72
+ /**
73
+ * The timestamp (in milliseconds) when the task can be retried.
74
+ *
75
+ * Can be `null` if the retry time is not known.
76
+ */
77
+ public get retryAt(): number | null {
78
+ return this._retryAt ?? null;
79
+ }
80
+
81
+ // eslint-disable-next-line @typescript-eslint/naming-convention
82
+ public toJSON(): RateLimitErrorPlainObject {
83
+ return {
84
+ name: this.name,
85
+ message: this.message,
86
+ code: this._code,
87
+ retryAt: this._retryAt,
88
+ stack: this.stack,
89
+ };
90
+ }
91
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * An error thrown when running a task on a destroyed rate limiter.
3
+ */
4
+ export class RateLimiterDestroyedError extends Error {
5
+ constructor() {
6
+ super('Rate limiter has been destroyed and cannot be used.');
7
+ }
8
+ }