@wordpress/priority-queue
Version:
Generic browser priority queue.
186 lines (167 loc) • 5.06 kB
text/typescript
/**
* Internal dependencies
*/
import requestIdleCallback from './request-idle-callback';
/**
* Enqueued callback to invoke once idle time permits.
*/
export type WPPriorityQueueCallback = VoidFunction;
/**
* An object used to associate callbacks in a particular context grouping.
*/
export type WPPriorityQueueContext = object;
/**
* Interface for the priority queue instance.
*/
export interface WPPriorityQueue {
/**
* Add a callback to the queue for a given context.
*/
add: (
element: WPPriorityQueueContext,
item: WPPriorityQueueCallback
) => void;
/**
* Flush and run the callback for a given context immediately.
* @return true if a callback was run, false otherwise.
*/
flush: ( element: WPPriorityQueueContext ) => boolean;
/**
* Cancel (remove) the callback for a given context without running it.
* @return true if a callback was cancelled, false otherwise.
*/
cancel: ( element: WPPriorityQueueContext ) => boolean;
/**
* Reset the entire queue, clearing pending callbacks.
*/
reset: VoidFunction;
}
/**
* Creates a context-aware queue that only executes
* the last task of a given context.
*
* @example
*```js
* import { createQueue } from '@wordpress/priority-queue';
*
* const queue = createQueue();
*
* // Context objects.
* const ctx1 = {};
* const ctx2 = {};
*
* // For a given context in the queue, only the last callback is executed.
* queue.add( ctx1, () => console.log( 'This will be printed first' ) );
* queue.add( ctx2, () => console.log( 'This won\'t be printed' ) );
* queue.add( ctx2, () => console.log( 'This will be printed second' ) );
*```
*
* @return {WPPriorityQueue} Queue object with `add`, `flush` and `reset` methods.
*/
export const createQueue = (): WPPriorityQueue => {
const waitingList = new Map<
WPPriorityQueueContext,
WPPriorityQueueCallback
>();
let isRunning = false;
/**
* Callback to process as much queue as time permits.
*
* Map Iteration follows the original insertion order. This means that here
* we can iterate the queue and know that the first contexts which were
* added will be run first. On the other hand, if anyone adds a new callback
* for an existing context it will supplant the previously-set callback for
* that context because we reassigned that map key's value.
*
* In the case that a callback adds a new callback to its own context then
* the callback it adds will appear at the end of the iteration and will be
* run only after all other existing contexts have finished executing.
*
* @param {IdleDeadline|number} deadline Idle callback deadline object, or
* animation frame timestamp.
*/
const runWaitingList = ( deadline: IdleDeadline | number ): void => {
for ( const [ nextElement, callback ] of waitingList ) {
waitingList.delete( nextElement );
callback();
if (
'number' === typeof deadline ||
deadline.timeRemaining() <= 0
) {
break;
}
}
if ( waitingList.size === 0 ) {
isRunning = false;
return;
}
requestIdleCallback( runWaitingList );
};
/**
* Add a callback to the queue for a given context.
*
* If errors with undefined callbacks are encountered double check that
* all of your useSelect calls have the right dependencies set correctly
* in their second parameter. Missing dependencies can cause unexpected
* loops and race conditions in the queue.
*
* @param {WPPriorityQueueContext} element Context object.
* @param {WPPriorityQueueCallback} item Callback function.
*/
const add: WPPriorityQueue[ 'add' ] = (
element: WPPriorityQueueContext,
item: WPPriorityQueueCallback
) => {
waitingList.set( element, item );
if ( ! isRunning ) {
isRunning = true;
requestIdleCallback( runWaitingList );
}
};
/**
* Flushes queue for a given context, returning true if the flush was
* performed, or false if there is no queue for the given context.
*
* @param {WPPriorityQueueContext} element Context object.
*
* @return {boolean} Whether flush was performed.
*/
const flush: WPPriorityQueue[ 'flush' ] = (
element: WPPriorityQueueContext
) => {
const callback = waitingList.get( element );
if ( undefined === callback ) {
return false;
}
waitingList.delete( element );
callback();
return true;
};
/**
* Clears the queue for a given context, cancelling the callbacks without
* executing them. Returns `true` if there were scheduled callbacks to cancel,
* or `false` if there was is no queue for the given context.
*
* @param {WPPriorityQueueContext} element Context object.
*
* @return {boolean} Whether any callbacks got cancelled.
*/
const cancel: WPPriorityQueue[ 'cancel' ] = (
element: WPPriorityQueueContext
) => {
return waitingList.delete( element );
};
/**
* Reset the queue without running the pending callbacks.
*/
const reset: WPPriorityQueue[ 'reset' ] = () => {
waitingList.clear();
isRunning = false;
};
return {
add,
flush,
cancel,
reset,
};
};