Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff58dd36f8 | |||
| 9ae45f51ce | |||
| 90426607a4 | |||
| 28c96ba9b9 | |||
| 4db9ab23eb | |||
| 3483a6c60e | |||
| ea5d7239dd | |||
| 1cf89ebcf6 | |||
| 54a14456e1 |
@@ -0,0 +1,66 @@
|
||||
# RateLimiter
|
||||
Simple but effective way to rate limit Tasks in JavaScript. Anything can be rate limited,
|
||||
|
||||
## Features
|
||||
- Rate limiting for anything!
|
||||
- Looks nice I guess?
|
||||
- That's about it.
|
||||
|
||||
## Usage
|
||||
```js
|
||||
var { RateLimiter } = require("@xaymar/ratelimiter");
|
||||
|
||||
let limitMany = new RateLimiter(4);
|
||||
let limitOne = new RateLimiter(1);
|
||||
|
||||
for (let idx = 0; idx < 3; idx++) {
|
||||
limitOne.queue(async () => {
|
||||
console.log("Only 1 of this can occur every 1s.");
|
||||
await new Promise((resolve, reject) => {setTimeout(() => {resolve();}, 1000);});
|
||||
});
|
||||
}
|
||||
|
||||
for (let idx = 0; idx < 10; idx++) {
|
||||
limitMany.queue(async () => {
|
||||
console.log("This however can occur many times.");
|
||||
await new Promise((resolve, reject) => {setTimeout(() => {resolve();}, 1000);});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## FAQ
|
||||
### Why did you create this?
|
||||
Multiple reasons, but here's two of the biggest examples:
|
||||
|
||||
1. A script that was supposed to automatically help me generate the proper Copyright notice headers ended up deleting files, or creating empty headers. Limiting the numbers of sub-processes to 1 for the version control binary, and the number of parallel file handles to the number of CPUs significantly improved the stability. No more empty files, no more empty headers!
|
||||
2. Some resources are only available in limited quantity, such as encoder instances on NVIDIA GPUs, or CPU cores. Often it makes sense to rate limit to that limit, instead of pushing as much data through as possible and then ending up slower than if you did everything sequentially. Especially when it comes to heavy and complex tasks, like encoding.
|
||||
|
||||
### Does this support WebWorkers?
|
||||
No, but it is relatively easy to do without official support. See the example below:
|
||||
|
||||
```js
|
||||
// main.js
|
||||
var { RateLimiter } = require("@xaymar/ratelimiter");
|
||||
|
||||
let worker = new Worker("worker.js");
|
||||
let workerRL = new RateLimiter(1);
|
||||
|
||||
worker.onmessage = (event) => {
|
||||
worker.resolve(event);
|
||||
}
|
||||
workerRL.queue(async () => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
worker.resolve = resolve;
|
||||
worker.reject = reject;
|
||||
worker.postMessage("Request");
|
||||
})
|
||||
});
|
||||
|
||||
// worker.js
|
||||
self.onmessage = (event) => {
|
||||
self.postMessage("Reply");
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
Available under GPLv3 as well as a commercial license. Contact `info@xaymar.com` for more information.
|
||||
Generated
+10
-6
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "ratelimiter",
|
||||
"version": "1.0.0",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ratelimiter",
|
||||
"version": "1.0.0",
|
||||
"license": "BSD",
|
||||
"version": "0.3.0",
|
||||
"license": "GPLv3",
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^18.15.3",
|
||||
@@ -15,6 +15,10 @@
|
||||
"@typescript-eslint/parser": "^5.43.0",
|
||||
"eslint": "^8.27.0",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/xaymar"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
@@ -1643,9 +1647,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
|
||||
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
||||
+9
-4
@@ -1,7 +1,14 @@
|
||||
{
|
||||
"name": "@xaymar/ratelimiter",
|
||||
"version": "0.1.0",
|
||||
"author": "Michael Fabian 'Xaymar' Dirks <info@xaymar.com>",
|
||||
"description": "A simple but effective way to rate limit Tasks in JavaScript.",
|
||||
"license": "GPLv3",
|
||||
"repository": "https://github.com/Xaymar/js-ratelimiter",
|
||||
"version": "0.3.0",
|
||||
"funding": {
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/xaymar"
|
||||
},
|
||||
"main": "generated/ratelimiter.js",
|
||||
"scripts": {
|
||||
"lint": "npx eslint ./source --ext .ts,.mts",
|
||||
@@ -9,10 +16,8 @@
|
||||
"compile": "npx tsc",
|
||||
"build": "npm run fix && npm run compile",
|
||||
"prepublish": "npm run build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "node ./tests/test.js"
|
||||
},
|
||||
"author": "Michael Fabian 'Xaymar' Dirks <info@xaymar.com>",
|
||||
"license": "GPLv3",
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^18.15.3",
|
||||
|
||||
+16
-3
@@ -24,15 +24,22 @@ interface RateLimiterInstance {
|
||||
solver?: Promise<any>,
|
||||
}
|
||||
|
||||
export type RateLimiterAsyncExecutor = (...args: any[]) => Promise<any>;
|
||||
export type RateLimiterSyncExecutor = (...args: any[]) => any;
|
||||
export type RateLimiterExecutor = RateLimiterSyncExecutor | RateLimiterAsyncExecutor;
|
||||
type RateLimiterAsyncExecutor = (...args: any[]) => Promise<any>;
|
||||
type RateLimiterSyncExecutor = (...args: any[]) => any;
|
||||
type RateLimiterExecutor = RateLimiterSyncExecutor | RateLimiterAsyncExecutor;
|
||||
|
||||
/** A simple but effective way to rate limit Tasks.
|
||||
*
|
||||
*/
|
||||
export class RateLimiter {
|
||||
private _maximum: number = 0;
|
||||
private _available: number = 0;
|
||||
private _instances: any[];
|
||||
|
||||
/** Create a new instance of a RateLimiter.
|
||||
*
|
||||
* @param limit The maximum number of tasks that should run in parallel. The Default is 2/3rds of the available CPU threads.
|
||||
*/
|
||||
constructor(limit?: number) {
|
||||
if (!limit) {
|
||||
this._maximum = Math.ceil(Math.max(1, os.cpus().length / 3 * 2));
|
||||
@@ -43,6 +50,12 @@ export class RateLimiter {
|
||||
this._instances = [];
|
||||
}
|
||||
|
||||
/** Queue up a new task.
|
||||
*
|
||||
* @param executor The function that should be run eventually.
|
||||
* @param args Optional arguments to pass to the function.
|
||||
* @returns A Promise that resolves with the result of the function.
|
||||
*/
|
||||
async queue(executor: RateLimiterExecutor, ...args: any[]) {
|
||||
// Use async/await to find a free slot.
|
||||
while (this._available == 0) {
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
// May require `npm link`.
|
||||
const { RateLimiter } = require("../generated/ratelimiter");
|
||||
|
||||
async function asyncRunTest(fn, ...args) {
|
||||
try {
|
||||
let msg = await fn(...args);
|
||||
if (msg) {
|
||||
console.log(`✅ ${fn.name}(${args}): ${msg}`);
|
||||
} else {
|
||||
console.log(`✅ ${fn.name}(${args})`);
|
||||
}
|
||||
} catch (ex) {
|
||||
console.log(`❌ ${fn.name}(${args})`)
|
||||
console.error(ex);
|
||||
process.exitCode++;
|
||||
}
|
||||
}
|
||||
function runTest(fn, ...args) {
|
||||
try {
|
||||
let msg = fn(...args);
|
||||
if (msg) {
|
||||
console.log(`✅ ${fn.name}(${args}): ${msg}`);
|
||||
} else {
|
||||
console.log(`✅ ${fn.name}(${args})`);
|
||||
}
|
||||
} catch (ex) {
|
||||
console.log(`❌ ${fn.name}(${args})`);
|
||||
console.error(ex);
|
||||
process.exitCode++;
|
||||
}
|
||||
}
|
||||
async function delay(time) {
|
||||
await new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, time);
|
||||
});
|
||||
}
|
||||
|
||||
function test_Construct(limit) {
|
||||
let rl = new RateLimiter(limit);
|
||||
}
|
||||
|
||||
async function test_TaskLimit(tasks, limit) {
|
||||
let rl = new RateLimiter(limit);
|
||||
let p = new Array();
|
||||
for (let idx = 0; idx < tasks; idx++) {
|
||||
p.push(rl.queue(() => {
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
let r = await Promise.all(p);
|
||||
for (res of r) {
|
||||
if (res !== true) {
|
||||
throw new Error("Result is unexpected.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function test_LimitEnforcement(tasks, limit) {
|
||||
let rl = new RateLimiter(limit);
|
||||
let value = 0;
|
||||
let time = 50;
|
||||
|
||||
let records = [];
|
||||
let t = setInterval(() => {
|
||||
records.push([
|
||||
value, Date.now()
|
||||
]);
|
||||
}, 25);
|
||||
|
||||
let p = new Array();
|
||||
for (let idx = 0; idx < tasks; idx++) {
|
||||
p.push(rl.queue(async () => {
|
||||
await delay(time);
|
||||
value++;
|
||||
}));
|
||||
}
|
||||
await Promise.all(p);
|
||||
|
||||
clearInterval(t);
|
||||
|
||||
let totalTime = 0;
|
||||
let totalValue = 0;
|
||||
for (let n = 1; n < records.length; n++) {
|
||||
let pRecord = records[n - 1];
|
||||
let cRecord = records[n];
|
||||
|
||||
let deltaTime = cRecord[1] - pRecord[1];
|
||||
let deltaValue = cRecord[0] - pRecord[0];
|
||||
|
||||
totalTime += deltaTime;
|
||||
totalValue += deltaValue;
|
||||
}
|
||||
let averageChange = totalValue / totalTime;
|
||||
let normalizedChange = averageChange * time;
|
||||
// setTimeout loses accuracy over time, not sure what the solution is. Accepting +-1 for now.
|
||||
if ((normalizedChange < (limit - 1.)) || (normalizedChange > (limit + 1.))) {
|
||||
throw new Error(`Outside acceptable range (${normalizedChange} is not equal to ${limit}).`);
|
||||
} else {
|
||||
return `Within acceptable range (${normalizedChange} is roughly equal to ${limit}).`
|
||||
}
|
||||
}
|
||||
|
||||
(async function() {
|
||||
process.exitCode = 0;
|
||||
for (let limit = 1; limit <= 64; limit *= 2) {
|
||||
runTest(test_Construct, limit);
|
||||
}
|
||||
for (let limit = 1; limit <= 10; limit++) {
|
||||
for (let tasks = 1; tasks <= 64; tasks *= 2) {
|
||||
await asyncRunTest(test_TaskLimit, tasks, limit);
|
||||
}
|
||||
}
|
||||
for (let limit = 1; limit <= 10; limit++) {
|
||||
await asyncRunTest(test_LimitEnforcement, 100, limit);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user