Coder Social home page Coder Social logo

p-throttle's Introduction

p-throttle

Throttle promise-returning & async functions

It also works with normal functions.

It rate-limits function calls without discarding them, making it ideal for external API interactions where avoiding call loss is crucial.

Install

npm install p-throttle

Usage

Here, the throttled function is only called twice a second:

import pThrottle from 'p-throttle';

const now = Date.now();

const throttle = pThrottle({
	limit: 2,
	interval: 1000
});

const throttled = throttle(async index => {
	const secDiff = ((Date.now() - now) / 1000).toFixed();
	return `${index}: ${secDiff}s`;
});

for (let index = 1; index <= 6; index++) {
	(async () => {
		console.log(await throttled(index));
	})();
}
//=> 1: 0s
//=> 2: 0s
//=> 3: 1s
//=> 4: 1s
//=> 5: 2s
//=> 6: 2s

API

pThrottle(options)

Returns a throttle function.

options

Type: object

Both the limit and interval options must be specified.

limit

Type: number

The maximum number of calls within an interval.

interval

Type: number

The timespan for limit in milliseconds.

strict

Type: boolean
Default: false

Use a strict, more resource intensive, throttling algorithm. The default algorithm uses a windowed approach that will work correctly in most cases, limiting the total number of calls at the specified limit per interval window. The strict algorithm throttles each call individually, ensuring the limit is not exceeded for any interval.

onDelay

Type: Function

Get notified when function calls are delayed due to exceeding the limit of allowed calls within the given interval.

Can be useful for monitoring the throttling efficiency.

In the following example, the third call gets delayed and triggers the onDelay callback:

import pThrottle from 'p-throttle';

const throttle = pThrottle({
	limit: 2,
	interval: 1000,
	onDelay: () => {
		console.log('Reached interval limit, call is delayed');
	},
});

const throttled = throttle(() => {
	console.log('Executing...');
});

await throttled();
await throttled();
await throttled();
//=> Executing...
//=> Executing...
//=> Reached interval limit, call is delayed
//=> Executing...

throttle(function_)

Returns a throttled version of function_.

function_

Type: Function

A promise-returning/async function or a normal function.

throttledFn.abort()

Abort pending executions. All unresolved promises are rejected with a pThrottle.AbortError error.

throttledFn.isEnabled

Type: boolean
Default: true

Whether future function calls should be throttled and count towards throttling thresholds.

throttledFn.queueSize

Type: number

The number of queued items waiting to be executed.

Related

  • p-debounce - Debounce promise-returning & async functions
  • p-limit - Run multiple promise-returning & async functions with limited concurrency
  • p-memoize - Memoize promise-returning & async functions
  • More…

p-throttle's People

Contributors

bendingbender avatar darcyparker avatar dormeiri avatar edy avatar jsau- avatar mikemcbride avatar mvila avatar richienb avatar samverschueren avatar schinkowitch avatar shinyaohira avatar sindresorhus avatar szmarczak avatar tehshrike avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

p-throttle's Issues

Wrapping generic functions inside `throttle`

Generic functions aren’t easy to deal with, especially when it comes to passing them as callbacks. Suppose we have a fetchJSON method with wraps around fetch, decodes JSON responses and throws a custom ResponseError when the response returns with a non-ok HTTP status code:

export type FetchJSONParams = Parameters<typeof fetch>;
export type FetchJSONResult<T> =
  | { data: T; error: null }
  | { data: undefined; error: unknown };

export async function fetchJSON<T = unknown>(
  ...args: FetchJSONParams
): Promise<FetchJSONResult<T>> {
  try {
    const res = await fetch(...args);
    if (!res.ok) {
      throw new ResponseError(res);
    }
    const data = await res.json();
    return { data, error: null };
  } catch (error) {
    return { data: undefined, error };
  }
}

When trying to create a throttled variant out of that as instructed by the readme, T gets assigned to its default value (unknown) automatically, even though that isn’t the desired behavior:

import pThrottle from "p-throttle";

const throttle = pThrottle({ limit: 10, interval: 1000 });
const throttledFetchJSON = throttle(fetchJSON);

throttledFetchJSON won’t have a generic parameter as a result as they can’t be inferred with the current version of TypeScript (4.5). However, there’s a hacky solution to the problem:

const throttle = pThrottle({ limit: 10, interval: 1000 });
const throttledFetchJSON = throttle(fetchJSON) as {
  <T>(...args: FetchJSONParams): Promise<FetchJSONResult<T>>;
} & {
  [key in keyof ThrottledFunction<unknown[], unknown>]: ThrottledFunction<
    unknown[],
    unknown
  >[key];
};

I think the usage of this call signature:

{
  <T>(...args: FetchJSONParams): Promise<FetchJSONResult<T>>;
}

should be documented inside the readme. For the second part, an auxiliary type could be exported from the library, e.g.:

// This could also be reused for defining `ThrottledFunction`
export type ThrottledFunctionProperties = {
  /**
    Whether future function calls should be throttled or count towards throttling thresholds.
    @default true
    */
  isEnabled: boolean;

  /**
    Abort pending executions. All unresolved promises are rejected with a `pThrottle.AbortError` error.
  */
  abort(): void;
};

Example with Promise.all

I'm not sure I understand how this works with a Promise.all

async function promiseAllGentle(arr, limit = 5, interval = 50) {
  const throttle = pThrottle({
    limit,
    interval,
  });

  const throttled = throttle((index) => arr[index]);

  const promises = arr.map((d, i) => throttled(i));

  return Promise.all(promises);
}

(async () => {
  const now = Date.now();

  const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => (
    new Promise((res) => setTimeout(() => {
      const secDiff = ((Date.now() - now) / 1000).toFixed();
      console.log(`${i}: ${secDiff}s`);
      return res(secDiff);
    }, 100))
  ));

  const res = await promiseAllGentle(arr, 2, 900);
  console.log(res);
})();

Actual Result:

0: 0s
1: 0s
2: 0s
3: 0s
4: 0s
5: 0s
6: 0s
7: 0s
8: 0s
9: 0s
[
  '0', '0', '0', '0',
  '0', '0', '0', '0',
  '0', '0'
]

Expected Result:

0: 0s
1: 0s
2: 1s
3: 1s
4: 2s
5: 2s
6: 3s
7: 3s
8: 4s
9: 4s
[
  '0', '0', '1', '1',
  '2', '2', '3', '3',
  '4', '4'
]

Am I missing something?

Throttle exceeds limit after a delay

I'm making API calls to a rate-limited endpoint (1000 requests/minute). I set p-throttle to allow 900 requests/limit and for a few minutes it seems to work fine. But after that the rate spikes to about 1100/minute, and shortly afterwards we get 429s from the API for exceeding their limit. I'm not sure why this happens. We're using the strict mode mentioned in #26.

I can provide a demo if necessary.

Throttled function can exhaust available memory

Consider what would happen if the throttled function is continuously called more often than the delay:

const foo = throttle(() => {...}, 1000)
setInterval(foo, 10)

p-throttle would keep queuing up calls faster than it can process them, and eventually the queue would run out of memory.

While it would be ridiculous to set an interval like this, it illustrates the fact p-throttle's memory usage is unbounded. If a user kept moving the mouse for a long time, and the events were coming in every 10 ms, then browser memory usage would continue to increase as long as the user kept moving the mouse.

lodash-throttle doesn't have this problem because it doesn't queue up calls; it just calls the throttled function once with the most recent arguments to foo in each window of time.

I'm not sure you intended for the semantics of p-throttle to differ so significantly from lodash-throttle, which is what devs are probably most familiar with.

If you did intend for this to have an unbounded queue, it would be helpful to warn about potential memory exhaustion to the README. Otherwise, you might want to make p-throttle behave more like lodash-throttle.

Must use import to load ES Module

I'm using the sample code and got this error:

import pThrottle from 'p-throttle';

const now = Date.now();

const throttle = pThrottle({
	limit: 2,
	interval: 1000
});

Found similar issues here
standard-things/esm#868
knex/knex#3571

Must use import to load ES Module: /var/task/node_modules/p-throttle/index.js require() of ES modules is not supported. require() of /var/task/node_modules/p-throttle/index.js from /var/task/app.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules. Instead rename index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /var/task/node_modules/p-throttle/package.json.

Typings seems incorrect

Maybe I missed something, but the function returned by throttle is not type-compatible with the original, which seems kinda wrong.

I'm trying to do something like this:

const throttledPost = pThrottle(got.post, 10, 30000);
...
const post = shouldThrottle ? throttledPost : got.post; // ts error here, incompatible types
post('/some/api, ...);

Is there something I missed, like a providing types for pThrottle or is this a bug?
Thanks for helping.

Why is the abort method on the throttledFn instead of the throttle fun

It is confusing if throttledFn.abort() stops all pending calls to execute since it is a method on a single throttledFn.

I'm trying to schedule a bunch of http calls in an array and then calling Promise.all :

const throttle = pThrottle(...);
cons arr = [1,2,3];
const promises = arr.map( num => throttle(makeHttpReq(num)) );
try {
 await Promise.all(promises)
} catch (e) {
 promises[0].abort() // seems weird that I need a throttleFn to achieve this
}

Maybe I'm trying to use this lib for something it wasn't meant to do ?

New ways to prevent starvation of throttled calls

Continuing #2, I am concerned with the simple scenario where the rate at which executions are being made exceeds the rate at which they are resolved due to the throttling config. This time, the concern is not around memory exhaustion, but around the ever-growing delay in executing the next call being made.

Using the abort function is a way to deal with this situation, with two drawbacks as I see them:

  • it is hard to employ this 'flushing' with precision. For example, periodically checking the throttler to see how many pending executions are there, 'flushing' only several of them, choosing whether to 'flush' by FIFO or LIFO, etc.
  • when pending executions are aborted, they reject with an pThrottle.AbortError. I'd love to have an option to have aborted executions resolve with a value that depends on the execution arguments.

As a solution, I'm basically thinking of adding a way to peek into the throttledFn object, and extending the abort function with the following options argument:

throttledFn.countPending(): Number

throttledFn.abort({
  count: Number, // default undefined, meaning to abort all pending executions
  applyToLastInvocations: Boolean, // default false, applies to earliest invocations  
  resolveWithValue: Function, // a non-async function that takes the `Fn` arguments of each execution, and resolves the aborted execution with `resolveWithValue.apply(args)`. Default undefined, meaning to reject with `pThrottle.AbortError`
})

Would appreciate feedback on relevance, direction, and anything else.

Throttle limit exceeded after a delay

I am calling an external service with a limit of 10 calls per second. I have configured the throttle as
throttle = pThrottle({ limit: 10, interval: 1010 })

Occasionally we are receiving a throttling error from the external service. Checking our logs, the service was called 11 times within a period of 980 ms.

The following mocha test reproduces the issue:

'use strict';

const pThrottle = require('p-throttle');

require('should');

describe('p-throttle', () => {
    let throttle,
        throttled;

    beforeEach(() => {
        throttle = pThrottle({
                limit: 10,
                interval: 1010
            });
        throttled = throttle(i => {return {index: i, executed: Date.now()}});
    });

    it('throttles after delay', async function () {
        this.timeout(3000);

        const initPromises = [],
            promises = [];

        for (let i = 0; i < 11; i++) {
            initPromises.push(throttled(i));
        }

        await Promise.all(initPromises);

        await delay(50);

        const start = Date.now()

        for (let i = 0; i < 11; i++) {
            promises.push(throttled(i));
        }
        const results = await Promise.all(promises);

        results.forEach((result,index) => {
            console.log(result);
            if (index < 10) {
                (result.executed - start).should.be.within(0, 1010);
            } else {
                (result.executed - results[index - 10].executed).should.be.within(1000, 1020);
            }
        });
    });
});

function delay (ms) {
    return new Promise(resolve => { setTimeout(resolve, ms); });
}

Consider aligning API with p-limit

I really like the design of the p-limit API whereby you create a limiter and pass in a fn as an argument e.g.

const limiter = pLimit(3);
const result1 = limiter(() => doSomething(1));
const result2 = limiter(() => doSomething(2));
const result3 = limiter(() => doSomethingElse(3)); // Doesn't care about what the fn does

I wondered if you'd be interested in aligning p-throttle with p-limit?

TimeoutOverflowWarning leading to performance issue

When using this module to throttle an often called method I noticed that after a while a TimeoutOverflowWarning would occur:

(node:25711) TimeoutOverflowWarning: 2446812003 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.

It can easily be reproduced using the following script which forces the overflow to occur after about 40 seconds. The problem is that the getDelay method keeps returing a larger timeout value on every call. As it is never reset it will eventually become too large to fit in a 32-bit integer. The setTimeout will default to every 1ms which has a negative impact on performance.

import PThrottle from "p-throttle";

const throttle = PThrottle({
  limit: 1,
  interval: 60000,
  strict: true,
});

const throttled = throttle(() => {
  return true;
});

(async () => {
  setInterval(async () => {
    throttled();
  }, 1);
})();

For now I've switched to lodash.throttle which does not suffer from this issue, but wanted to let you know anyway.

throttle across multiple processes

Any insights how I would do this if I am hitting an API from multiple node processes and thus need to limit the execution across all processes?

Multiple use in same script

Hi all,

Is there a way to use this module several times with different (limit/interval) in the same script?

Example:

import pThrottle from 'p-throttle';

const now = Date.now();

const throttle = pThrottle({
	limit: 2,
	interval: 1000
});
const throttle2 = pThrottle({
	limit: 2,
	interval: 6000
});

const throttled = throttle(async index => {
	const secDiff = ((Date.now() - now) / 1000).toFixed();
	return `${index}: ${secDiff}s`;
});

const throttled2 = throttle2(async index => {
	const secDiff = ((Date.now() - now) / 1000).toFixed();
	return `${index}: ${secDiff}s`;
});


for (let index = 1; index <= 100; index++) {
	(async () => {
		console.log(await throttled(index));
                console.log(await throttled2(index));
	})();
}

In this case, on both functions throttled/throttled2, throttle interval used will be 6000.

Any idea?

Thks

Temporary disable throttle

Is it possible to disable throttle for a specific call?

const pThrottle = require('p-throttle');

const now = Date.now();

const throttled = pThrottle(index => {
	const secDiff = ((Date.now() - now) / 1000).toFixed();

	if (index===4){
		return pThrottle.immediate(Promise.resolve(`${index}: ${secDiff}s`));
	}

	return Promise.resolve(`${index}: ${secDiff}s`);
}, 2, 1000);

for (let i = 1; i <= 6; i++) {
	throttled(i).then(console.log);
}

//=> 1: 0s
//=> 2: 0s
//=> 3: 1s
//=> 4: 1s
//=> 5: 1s
//=> 6: 2s

You can see that 5th immediately executed

Minor optimization - don't call `setTimeout` if not needed

When getDelay() returns 0 there is no need to call setTimeout, it creates redundant event-loop event.

https://github.com/sindresorhus/p-throttle/blob/3dffe707c0061370be8ce0a8c687bd032b6acaef/index.js#L83-85

My suggestion:

  • Check first if the result of getDelay() is greater than 0
  • If yes, run setTimeout and insert it to the queue.
  • Otherwise, run execute directly, call queue.delete(timeoutId); only if timeoutId is defined.

WDYT?

I can open a PR if it helps.

typing error with the latest version 4.1.0

TypeScript error occurred after upgrading with the latest version 4.1.0.

Here is the modified version of sample code in readme.md

const pThrottle = require('p-throttle');

const now = Date.now();

const throttle = pThrottle({
  limit: 2,
  interval: 1000,
});

const func = (index: number, tag: string): Promise<string> => {
  const secDiff = ((Date.now() - now) / 1000).toFixed();
  return Promise.resolve(`${tag} ${index}: ${secDiff}s`);
};

const throttled = throttle<number, string>(func);

I got a following error.

error TS2345: Argument of type '(index: number, tag: string) => Promise<string>' is not assignable to parameter of type '(...arguments: number[]) => string'.
  Types of parameters 'tag' and 'arguments' are incompatible.
    Type 'number' is not assignable to type 'string'.

It seems that current definition assumes all types of arguments are the same.

More Flexible Function Arguments for Throttle

throttle(fetch) is having a typescript definition issue right now due to lack of flexibility with the definitions.

On index.d.ts on line 13~14 and 80~81:
Rather than having Argument in the defs, change it to Arguments, and rather than returning ...arguments:Argument[], have it return ...arguments: Arguments. This should resolve its lack of flexibility. I too would be willing to dive in and open a PR if necessary.

Add a callback hook when throttled

I would like to observe every time I reach the throttling limit and get delayed.

I think I can achieve it by calling a callback function when getDelay() is greater than zero here:

timeoutId = setTimeout(execute, getDelay());

The callback function can be passed as one of the options in:

export default function pThrottle({limit, interval, strict}) {

I suggest something like this: onDelay: (info: { delay: number, queueSize: number }) => void | Promise<void>, WDYT?

I can open a PR if it helps.

Edit: I can live without passing the info to the callback, so this also works for my use-case: onDelay: () => void | Promise<void>

Publish es5 version

Thanks for the awesome throttle implementation. Would you mind also publishing an es5-compatible version to npm? Is this something you would consider doing? It would be nice to get rid of the extra transpilation step.

Weighted throttle

Hello everyone. We are using Storyblok as our CMS and access the data using the GraphQL API. Their limits are point based, where an empty connection is 1 point and every query inside that connection costs another query. Yesterday we reached the limit and half our website was down. To combat this issue we combined queries (less points) and added a throttle to prevent reaching the limit.

The problem we are facing is that right now, we have set the limit to 10 requests per second but we don't reach our full potential because they use a point system and not a request system. We can use 100 points per second, but requesting 1 table costs 2 points. Requesting 3 tables costs 4 points. Therefor my question is, would it be possible to give a throttle instance some kind of weight (point), so we can reach the full potential of the API and therefor decrease loading times?

Thank you in advance!

Link in Readme misleading

Hey, thanks for the library, it is exactly what I was looking for.

In your README you link to https://css-tricks.com/debouncing-throttling-explained-examples/ ... but there the throttle of lodash/underscore.js is explained. That confused me a little, as their throttle throws function calls away and is not usable to throttle api calls. Here the function calls are queued. (That's what I needed, but the link made me doubt the library is what i wanted.) Maybe remove the link from the docs?

For the interested you can see the difference here:

var _ = require('lodash');
var pThrottle = require('p-throttle');
var now = Date.now();

function somefunction(index){
	const secDiff = ((Date.now() - now) / 1000).toFixed();
	return Promise.resolve(`${index}: ${secDiff}s`);
}
var lodashThrottle = _.throttle(somefunction, 500);
var pThrottle = pThrottle(somefunction, 1, 500);

for (let i = 1; i <= 4; i++) {
	lodashThrottle(i).then(r => console.log('lodash ' + r));
}

for (let i = 1; i <= 4; i++) {
	pThrottle(i).then(r => console.log('p ' + r));
}

//lodash 1: 0s
//lodash 1: 0s
//lodash 1: 0s
//lodash 1: 0s
//p 1: 1s
//p 2: 1s
//p 3: 1s
//p 4: 2s

Use `Performance.now()` instead of `Date.now()`

I see that the package uses Date.now() for calculating the delta between the current time and the beginning of the window.

const now = Date.now();

const now = Date.now();

When calculating times delta it is recommended to use Performance.now() as it is monotonic, more precise, and more system agnostic.

WDYT about changing Date.now() to Performance.now()?

I can open a PR if needed

Export in standard Javascript

When running Jest with p-throttle, I get the following error:

Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

Details:

 ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){export class AbortError extends Error {

Can you export a common JS version of p-throttle ?

Timeout doesn't get cleaned up and prevent Node process to complete

After executing the throttled function at least one timer is left running and prevent the Node process to exit.

The only way to get Node to exit is to use process.exit().

I looked at it but not sure how to fix it. I guess the timeout should not be created pre-emptively.

Bizarre behavior with Bluebird

I just spent a few hours debugging this so thought it may help someone. When the global Promise object is replaced with bluebird, the async function mysteriously never resolves.

Code to repro:

global.Promise = require('bluebird')
const pThrottle = require('p-throttle')
const got = require('got')

const throttled = pThrottle(got, 1, 1000)

async function main() {
  for (let i = 1; i <= 3; i++) {
    const { headers } = await throttled('http://httpbin.org/response-headers')
    console.log(headers.date)
  }
  console.log('~End~')
}

main().catch(console.error)

When Promise is patched, the process just exits with code 0 and even ~End~ is not logged to the console.
When Promise isn't patched, or when pThrottle isn't used (just got), then it works as it should.

Tested this with node v8.9.0 and v9.1.0 on macOS and Linux.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.