p-queue

repository·main·Indexed 26 days ago

https://github.com/sindresorhus/p-queue

A promise queue with concurrency control for rate-limiting asynchronous or synchronous operations, such as API requests or CPU/memory intensive tasks. Version 9.3.3. Features include priority scheduling, interval-based rate limiting, task timeouts, and support for AbortSignal for task cancellation.

Tokens
5.9K
Snippets
21
Records
25
Agent score
38%

What's inside p-queue

  1. Implement backpressure using onSizeLessThan()

    main

    To prevent the queue from growing unbounded and causing memory issues when producers are faster than consumers, use .onSizeLessThan(). This allows you to wait for the queue to have space before adding more tasks.

    Note that .size counts queued items, while .pending counts running items. The total number of items in the queue is queue.size + queue.pending.

    const queue = new PQueue();
    
    // Wait for queue to have space before adding more
    await queue.onSizeLessThan(100);
    queue.add(() => someTask());
  2. Get results in the order they were added

    main

    p-queue executes tasks in priority order but does not guarantee completion order. If you need results to match the input order, wrap the task additions in Promise.all().

    import PQueue from 'p-queue';
    
    const queue = new PQueue({concurrency: 4});
    
    const tasks = [
    	() => fetchData(1), // May finish third
    	() => fetchData(2), // May finish first
    	() => fetchData(3), // May finish second
    ];
    
    const results = await Promise.all(
    	tasks.map(task => queue.add(task))
    );
    // results = [result1, result2, result3] ✅ Always in input order
  3. Cancel or remove a queued task using AbortSignal

    main

    Use AbortSignal for targeted cancellation. When a signal is aborted, a queued task is removed and the promise returned by .add() rejects.

    Important: Aborting only rejects the promise returned by .add(); it does not automatically stop the async work inside your function. For running tasks, you must handle the signal inside the function itself.

    Avoid using queue.clear() alone for cancellation, as it removes queued tasks but their .add() promises will never settle, causing dangling promises.

    import PQueue from 'p-queue';
    
    const queue = new PQueue();
    const controller = new AbortController();
    
    const promise = queue.add(({signal}) => doWork({signal}), {signal: controller.signal});
    
    controller.abort(); // Cancels if still queued; running tasks must handle `signal` themselves
  4. Debug a queue that stops processing tasks

    main

    If a queue stops processing, tasks may be hanging indefinitely and exhausting the concurrency limit.

    Debugging Strategies:

    1. Use Timeouts: Set a timeout in the PQueue constructor to prevent I/O operations from hanging forever.
    2. Monitor runningTasks: Use the queue.runningTasks property to inspect tasks that are currently executing. You can filter these by task.startTime to find stuck tasks.
    3. Monitor Saturation: Check queue.isSaturated to see if the queue is full.
    4. Track Lifecycle: Listen to the completed and error events.
    5. Add IDs: Pass an id in the options object of .add() to make identifying tasks easier in logs.
    // 1. Add timeouts to prevent hanging
    const queue = new PQueue({
    	concurrency: 2,
    	timeout: 30000 // 30 seconds
    });
    
    // 2. Always add IDs to tasks for debugging
    queue.add(() => processItem(item), {id: `item-${item.id}`});
    
    // 3. Monitor stuck tasks using runningTasks
    setInterval(() => {
    	const now = Date.now();
    	const stuckTasks = queue.runningTasks.filter(task =>
    		now - task.startTime > 30000 // Running for over 30 seconds
    	);
    
    	if (stuckTasks.length > 0) {
    		console.error('Stuck tasks:', stuckTasks);
    	}
    
    	if (queue.isSaturated) {
    		console.warn(`Queue saturated: ${queue.pending} running, ${queue.size} waiting`);
    	}
    }, 60000);
    
    // 4. Track task lifecycle
    queue.on('completed', result => {
    	console.log('Task completed');
    });
    queue.on('error', error => {
    	console.error('Task failed:', error);
    });
  5. Workaround for Jest fake timers

    main

    Because p-queue uses queueMicrotask internally, Jest fake timers may not work as expected. Use a flushPromises helper to ensure microtasks are processed.

    const flushPromises = () => new Promise(resolve => setImmediate(resolve));
    
    jest.useFakeTimers();
    
    // ... your test code ...
    
    await jest.runAllTimersAsync();
    await flushPromises();
  6. Stream results in order using p-queue and p-mapIterable

    main

    To achieve progressive results that maintain input order while using p-queue for concurrency or priority management, combine p-queue with pMapIterable from the p-map package.

    import PQueue from 'p-queue';
    import {pMapIterable} from 'p-map';
    
    // Let p-queue handle concurrency
    const queue = new PQueue({concurrency: 4});
    
    for await (const result of pMapIterable(
    	items,
    	item => queue.add(() => fetchItem(item), {priority: item.priority})
    )) {
    	console.log(result); // Still in input order
    }
  7. Use p-queue for concurrency control

    main

    Use PQueue to limit the number of concurrent asynchronous or synchronous operations. This is useful for rate-limiting tasks like REST API calls or CPU/memory intensive operations.

    By default, you can specify the concurrency option in the constructor to control how many promises run at the same time.

    import PQueue from 'p-queue';
    import got from 'got';
    
    const queue = new PQueue({concurrency: 1});
    
    (async () => {
    	await queue.add(() => got('https://sindresorhus.com'));
    	console.log('Done: sindresorhus.com');
    })();
    
    (async () => {
    	await queue.add(() => got('https://avajs.dev'));
    	console.log('Done: avajs.dev');
    })();
  8. Perform bulk cancellation with a shared AbortController

    main

    To cancel multiple tasks at once, share a single AbortController across all tasks added to the queue. When controller.abort() is called, all queued tasks are removed and their .add() promises reject cleanly.

    import PQueue from 'p-queue';
    const queue = new PQueue({concurrency: 2});
    const controller = new AbortController();
    
    // All tasks share the same signal
    queue.add(({signal}) => doWork(signal), {signal: controller.signal}).catch(() => {});
    queue.add(({signal}) => doWork(signal), {signal: controller.signal}).catch(() => {});
    queue.add(({signal}) => doWork(signal), {signal: controller.signal}).catch(() => {});
    
    // Cancel all queued (and signal running) tasks — promises reject cleanly
    controller.abort();
  9. Monitor queue status and metrics

    main

    Access real-time information about the queue's state:

    • .size: Number of items waiting in the queue.
    • .pending: Number of items currently running.
    • .isPaused: Boolean indicating if the queue is paused.
    • .isRateLimited: Boolean indicating if the queue is currently rate-limited.
    • .isSaturated: Boolean indicating if all concurrency slots are occupied OR the queue is rate-limited and tasks are waiting.
    • .sizeBy(options): Returns the size of the queue filtered by specific options (e.g., {priority: 1}).
    • .runningTasks: Returns an array of objects containing info for currently executing tasks (id, priority, startTime, timeout, timeoutRemaining).
    // Check for backpressure
    if (queue.isSaturated) {
    	await queue.onSizeLessThan(queue.concurrency);
    }
    
    // Inspect running tasks
    console.log(queue.runningTasks);
  10. Update task priority with .setPriority()

    main

    Update the priority of a task using its id. This affects the execution order and requires a defined concurrency limit to be effective.

    • To prioritize: Pass a higher number.
    • To deprioritize: Pass a lower number.
    import PQueue from 'p-queue';
    
    const queue = new PQueue({concurrency: 1});
    
    queue.add(async () => '🦄', {priority: 1});
    queue.add(async () => '🦀', {priority: 0, id: '🦀'});
    
    // Move '🦀' to the front
    queue.setPriority('🦀', 2);
    
    // Move '🦀' to the end
    queue.setPriority('🦀', -1);