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
```typescript
import {Limiter} from '@nesterow/limiter'
import { Limiter } from "@nesterow/limiter";
const task = () => {
await fetch('https://my.api.xyz')
await fetch("https://my.api.xyz");
// ... write
}
};
const limiter = new Limiter({
limit: 10
})
limit: 10,
});
for (let i=0; i<100; i++) {
await limiter.process(task)
for (let i = 0; i < 100; i++) {
await limiter.process(task);
}
```
### Limit RPS

30
dist/limiter.js vendored
View file

@ -16,7 +16,7 @@ export class Limiter {
#maxRetry = 0;
#rps;
#onError;
constructor({ limit = 10, rps, maxRetry = 0, onError = undefined, }) {
constructor({ limit = 10, rps, maxRetry = 0, onError = undefined }) {
this.#limit = limit;
this.#rps = rps;
this.#maxRetry = maxRetry;
@ -41,15 +41,13 @@ export class Limiter {
try {
await Promise.all(this.#promises);
this.#promises = [];
}
catch (error) {
} catch (error) {
if (!this.#onError) {
throw error;
}
for (;;) {
const promise = this.#promises.pop();
if (!promise)
break;
if (!promise) break;
promise.catch(this.#onError);
}
}
@ -57,8 +55,7 @@ export class Limiter {
async process(...callbacks) {
for (;;) {
const item = callbacks.pop();
if (!item)
break;
if (!item) break;
if (this.#promisesCount >= this.#limit) {
await this.#execute();
}
@ -69,8 +66,7 @@ export class Limiter {
const res = await this.#limitRps(callback);
this.#promisesCount--;
return res;
}
catch (error) {
} catch (error) {
this.#promisesCount--;
if (this.#maxRetry > 0) {
this.#retryQueue.push({
@ -78,8 +74,7 @@ export class Limiter {
retries: item.retries ?? this.#maxRetry,
error: error,
});
}
else {
} else {
throw error;
}
}
@ -93,16 +88,15 @@ export class Limiter {
const retryItems = [];
for (;;) {
const item = this.#retryQueue.pop();
if (!item)
break;
if (!item) break;
if (item.retries > 0) {
item.retries--;
retryItems.push(item);
}
else if (this.#onError) {
this.#onError(new LimiterRetryError("Retry limit exceeded", item.error));
}
else {
} else if (this.#onError) {
this.#onError(
new LimiterRetryError("Retry limit exceeded", item.error),
);
} else {
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;
return {
process: jest.fn(async () => {
if (closed)
throw new Error("Connection closed");
if (closed) throw new Error("Connection closed");
//if (loading) throw new Error("Connection in use");
loading = true;
await send();
@ -31,9 +30,11 @@ test("Limiter: opens #limit of concurent connections", async () => {
});
const limiter = new Limiter({ limit: 3 });
const connections = Array.from({ length: 7 }, () => connection());
limiter.process(...connections.map((c) => {
limiter.process(
...connections.map((c) => {
return c.process;
}));
}),
);
await delay(0);
expect(limiter.length).toBe(3);
await delay(500);
@ -72,15 +73,18 @@ test("Limiter: limit RPS - requests are evenly distributed", async () => {
const connections = Array.from({ length: 45 }, () => connection());
let count = 0;
const timestamps = [];
await limiter.process(...connections.map((c) => {
await limiter.process(
...connections.map((c) => {
return () => {
++count;
timestamps.push(Date.now());
return c.process();
};
}));
}),
);
expect(count).toBe(45);
const diffsAvg = timestamps
const diffsAvg =
timestamps
.map((t, i) => {
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 connections = Array.from({ length: 6 }, () => connection());
try {
await limiter.process(...connections.map((c) => {
await limiter.process(
...connections.map((c) => {
return c.process;
}));
}
catch (e) {
}),
);
} catch (e) {
expect(e).toBe(1);
}
expect(limiter.length).toBe(0);
@ -113,15 +118,17 @@ test("Limiter: #onError, no trow", async () => {
close: jest.fn(() => Promise.resolve()),
delay: 500,
});
const onError = jest.fn(() => { });
const onError = jest.fn(() => {});
const limiter = new Limiter({
limit: 3,
onError,
});
const connections = Array.from({ length: 6 }, () => connection());
await limiter.process(...connections.map((c) => {
await limiter.process(
...connections.map((c) => {
return c.process;
}));
}),
);
expect(limiter.length).toBe(0);
expect(connections[0].send).toBeCalledTimes(6);
expect(onError).toBeCalledTimes(6);
@ -139,12 +146,13 @@ test("Limiter: #maxRetry, exit on fail", async () => {
const connections = Array.from({ length: 6 }, () => connection());
let count = 0;
try {
await limiter.process(...connections.map((c) => {
await limiter.process(
...connections.map((c) => {
++count;
return c.process;
}));
}
catch (e) {
}),
);
} catch (e) {
expect(e).toBeInstanceOf(LimiterRetryError);
}
expect(limiter.length).toBe(0);
@ -165,9 +173,11 @@ test("Limiter: #onError, #maxRetry", async () => {
onError,
});
const connections = Array.from({ length: 6 }, () => connection());
await limiter.process(...connections.map((c) => {
await limiter.process(
...connections.map((c) => {
return c.process;
}));
}),
);
expect(onError).toBeCalledTimes(6);
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;
}
}

View file

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