This commit is contained in:
Anton Nesterov 2024-07-11 01:20:10 +02:00
parent a6c579e429
commit 6884386903
6 changed files with 277 additions and 268 deletions

View file

@ -24,21 +24,20 @@ bun add github:nesterow/limiter # or pnpm
### Limit number of requests ### Limit number of requests
```typescript ```typescript
import {Limiter} from '@nesterow/limiter' import { Limiter } from "@nesterow/limiter";
const task = () => { const task = () => {
await fetch('https://my.api.xyz') await fetch("https://my.api.xyz");
// ... write // ... write
} };
const limiter = new Limiter({ const limiter = new Limiter({
limit: 10 limit: 10,
}) });
for (let i=0; i<100; i++) { for (let i = 0; i < 100; i++) {
await limiter.process(task) await limiter.process(task);
} }
``` ```
### Limit RPS ### Limit RPS

30
dist/limiter.js vendored
View file

@ -16,7 +16,7 @@ export class Limiter {
#maxRetry = 0; #maxRetry = 0;
#rps; #rps;
#onError; #onError;
constructor({ limit = 10, rps, maxRetry = 0, onError = undefined, }) { constructor({ limit = 10, rps, maxRetry = 0, onError = undefined }) {
this.#limit = limit; this.#limit = limit;
this.#rps = rps; this.#rps = rps;
this.#maxRetry = maxRetry; this.#maxRetry = maxRetry;
@ -41,15 +41,13 @@ export class Limiter {
try { try {
await Promise.all(this.#promises); await Promise.all(this.#promises);
this.#promises = []; this.#promises = [];
} } catch (error) {
catch (error) {
if (!this.#onError) { if (!this.#onError) {
throw error; throw error;
} }
for (;;) { for (;;) {
const promise = this.#promises.pop(); const promise = this.#promises.pop();
if (!promise) if (!promise) break;
break;
promise.catch(this.#onError); promise.catch(this.#onError);
} }
} }
@ -57,8 +55,7 @@ export class Limiter {
async process(...callbacks) { async process(...callbacks) {
for (;;) { for (;;) {
const item = callbacks.pop(); const item = callbacks.pop();
if (!item) if (!item) break;
break;
if (this.#promisesCount >= this.#limit) { if (this.#promisesCount >= this.#limit) {
await this.#execute(); await this.#execute();
} }
@ -69,8 +66,7 @@ export class Limiter {
const res = await this.#limitRps(callback); const res = await this.#limitRps(callback);
this.#promisesCount--; this.#promisesCount--;
return res; return res;
} } catch (error) {
catch (error) {
this.#promisesCount--; this.#promisesCount--;
if (this.#maxRetry > 0) { if (this.#maxRetry > 0) {
this.#retryQueue.push({ this.#retryQueue.push({
@ -78,8 +74,7 @@ export class Limiter {
retries: item.retries ?? this.#maxRetry, retries: item.retries ?? this.#maxRetry,
error: error, error: error,
}); });
} } else {
else {
throw error; throw error;
} }
} }
@ -93,16 +88,15 @@ export class Limiter {
const retryItems = []; const retryItems = [];
for (;;) { for (;;) {
const item = this.#retryQueue.pop(); const item = this.#retryQueue.pop();
if (!item) if (!item) break;
break;
if (item.retries > 0) { if (item.retries > 0) {
item.retries--; item.retries--;
retryItems.push(item); retryItems.push(item);
} } else if (this.#onError) {
else if (this.#onError) { this.#onError(
this.#onError(new LimiterRetryError("Retry limit exceeded", item.error)); new LimiterRetryError("Retry limit exceeded", item.error),
} );
else { } else {
throw new LimiterRetryError("Retry limit exceeded", item.error); throw new LimiterRetryError("Retry limit exceeded", item.error);
} }
} }

50
dist/limiter.test.js vendored
View file

@ -7,8 +7,7 @@ const setup = ({ send, close, delay = 300 }) => {
let loading = false; let loading = false;
return { return {
process: jest.fn(async () => { process: jest.fn(async () => {
if (closed) if (closed) throw new Error("Connection closed");
throw new Error("Connection closed");
//if (loading) throw new Error("Connection in use"); //if (loading) throw new Error("Connection in use");
loading = true; loading = true;
await send(); await send();
@ -31,9 +30,11 @@ test("Limiter: opens #limit of concurent connections", async () => {
}); });
const limiter = new Limiter({ limit: 3 }); const limiter = new Limiter({ limit: 3 });
const connections = Array.from({ length: 7 }, () => connection()); const connections = Array.from({ length: 7 }, () => connection());
limiter.process(...connections.map((c) => { limiter.process(
...connections.map((c) => {
return c.process; return c.process;
})); }),
);
await delay(0); await delay(0);
expect(limiter.length).toBe(3); expect(limiter.length).toBe(3);
await delay(500); await delay(500);
@ -72,15 +73,18 @@ test("Limiter: limit RPS - requests are evenly distributed", async () => {
const connections = Array.from({ length: 45 }, () => connection()); const connections = Array.from({ length: 45 }, () => connection());
let count = 0; let count = 0;
const timestamps = []; const timestamps = [];
await limiter.process(...connections.map((c) => { await limiter.process(
...connections.map((c) => {
return () => { return () => {
++count; ++count;
timestamps.push(Date.now()); timestamps.push(Date.now());
return c.process(); return c.process();
}; };
})); }),
);
expect(count).toBe(45); expect(count).toBe(45);
const diffsAvg = timestamps const diffsAvg =
timestamps
.map((t, i) => { .map((t, i) => {
return i === 0 ? 100 : t - timestamps[i - 1]; return i === 0 ? 100 : t - timestamps[i - 1];
}) })
@ -97,11 +101,12 @@ test("Limiter: throws an error by deafult", async () => {
const limiter = new Limiter({ limit: 3 }); const limiter = new Limiter({ limit: 3 });
const connections = Array.from({ length: 6 }, () => connection()); const connections = Array.from({ length: 6 }, () => connection());
try { try {
await limiter.process(...connections.map((c) => { await limiter.process(
...connections.map((c) => {
return c.process; return c.process;
})); }),
} );
catch (e) { } catch (e) {
expect(e).toBe(1); expect(e).toBe(1);
} }
expect(limiter.length).toBe(0); expect(limiter.length).toBe(0);
@ -113,15 +118,17 @@ test("Limiter: #onError, no trow", async () => {
close: jest.fn(() => Promise.resolve()), close: jest.fn(() => Promise.resolve()),
delay: 500, delay: 500,
}); });
const onError = jest.fn(() => { }); const onError = jest.fn(() => {});
const limiter = new Limiter({ const limiter = new Limiter({
limit: 3, limit: 3,
onError, onError,
}); });
const connections = Array.from({ length: 6 }, () => connection()); const connections = Array.from({ length: 6 }, () => connection());
await limiter.process(...connections.map((c) => { await limiter.process(
...connections.map((c) => {
return c.process; return c.process;
})); }),
);
expect(limiter.length).toBe(0); expect(limiter.length).toBe(0);
expect(connections[0].send).toBeCalledTimes(6); expect(connections[0].send).toBeCalledTimes(6);
expect(onError).toBeCalledTimes(6); expect(onError).toBeCalledTimes(6);
@ -139,12 +146,13 @@ test("Limiter: #maxRetry, exit on fail", async () => {
const connections = Array.from({ length: 6 }, () => connection()); const connections = Array.from({ length: 6 }, () => connection());
let count = 0; let count = 0;
try { try {
await limiter.process(...connections.map((c) => { await limiter.process(
...connections.map((c) => {
++count; ++count;
return c.process; return c.process;
})); }),
} );
catch (e) { } catch (e) {
expect(e).toBeInstanceOf(LimiterRetryError); expect(e).toBeInstanceOf(LimiterRetryError);
} }
expect(limiter.length).toBe(0); expect(limiter.length).toBe(0);
@ -165,9 +173,11 @@ test("Limiter: #onError, #maxRetry", async () => {
onError, onError,
}); });
const connections = Array.from({ length: 6 }, () => connection()); const connections = Array.from({ length: 6 }, () => connection());
await limiter.process(...connections.map((c) => { await limiter.process(
...connections.map((c) => {
return c.process; return c.process;
})); }),
);
expect(onError).toBeCalledTimes(6); expect(onError).toBeCalledTimes(6);
expect(error).toBeInstanceOf(LimiterRetryError); expect(error).toBeInstanceOf(LimiterRetryError);
}); });

5
jsr.json Normal file
View file

@ -0,0 +1,5 @@
{
"name": "@nesterow/limiter",
"version": "0.1.0",
"exports": "./limiter.ts"
}

View file

@ -140,7 +140,7 @@ export class Limiter implements ILimiter {
} }
} }
get length() { get length(): number {
return this.#promisesCount; return this.#promisesCount;
} }
} }

View file

@ -1,4 +1,5 @@
{ {
"version": "0.1.0",
"name": "@nesterow/limiter", "name": "@nesterow/limiter",
"module": "limiter.ts", "module": "limiter.ts",
"type": "module", "type": "module",