UNPKG

3d-tiles-renderer

Version:

https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification

346 lines (235 loc) 5.99 kB
import { Scheduler } from './Scheduler.js'; // Error thrown when a queued item's promise is rejected because the item was removed // before its callback could run. class PriorityQueueItemRemovedError extends DOMException { constructor() { super( 'PriorityQueue: Item removed', 'AbortError' ); } } /** * @callback PriorityCallback * @param {any} a * @param {any} b * @returns {number} */ /** * @callback SchedulingCallback * @param {Function} func */ /** * @callback ItemCallback * @param {any} item * @returns {Promise<any>|any} */ /** * @callback FilterCallback * @param {any} item * @returns {boolean} */ /** * Priority queue for scheduling async work with a concurrency limit. Items are * sorted by `priorityCallback` and dispatched up to `maxJobs` at a time. */ export class PriorityQueue { /** * returns whether tasks are queued or actively running * @readonly * @type {boolean} */ get running() { return this.items.length !== 0 || this.currJobs !== 0; } constructor() { /** * Maximum number of jobs that can run concurrently. * @type {number} * @default 6 */ this.maxJobs = 6; this.items = []; this.callbacks = new Map(); this.currJobs = 0; this.scheduled = false; /** * If true, job runs are automatically scheduled after `add` and after each job completes. * @type {boolean} * @default true */ this.autoUpdate = true; /** * Comparator used to sort queued items. Higher-priority items should sort last * (i.e. return positive when `itemA` should run before `itemB`). * @type {PriorityCallback|null} * @default null */ this.priorityCallback = null; this._schedulingCallback = func => { Scheduler.requestAnimationFrame( func ); }; this._runjobs = () => { this.scheduled = false; this.tryRunJobs(); }; } /** * Sorts the pending item list using `priorityCallback`, if set. */ sort() { const priorityCallback = this.priorityCallback; const items = this.items; if ( priorityCallback !== null ) { items.sort( priorityCallback ); } } /** * Returns whether the given item is currently queued. * @param {any} item * @returns {boolean} */ has( item ) { return this.callbacks.has( item ); } /** * Adds an item to the queue and returns a Promise that resolves when the item's * callback completes, or rejects if the item is removed before running. * @param {any} item * @param {ItemCallback} callback - Invoked with `item` when it is dequeued; may return a Promise * @returns {Promise<any>} */ add( item, callback ) { const data = { callback, reject: null, resolve: null, promise: null, }; data.promise = new Promise( ( resolve, reject ) => { const items = this.items; const callbacks = this.callbacks; data.resolve = resolve; data.reject = reject; items.unshift( item ); callbacks.set( item, data ); if ( this.autoUpdate ) { this.scheduleJobRun(); } } ); return data.promise; } /** * Removes an item from the queue, rejecting its promise with an `AbortError` DOMException. * @param {any} item */ remove( item ) { const items = this.items; const callbacks = this.callbacks; const index = items.indexOf( item ); if ( index !== - 1 ) { // reject the promise to ensure there are no dangling promises - add a // catch here to handle the case where the promise was never used anywhere // else. const info = callbacks.get( item ); info.promise.catch( err => { if ( err.name !== 'AbortError' ) { throw err; } } ); info.reject( new PriorityQueueItemRemovedError() ); items.splice( index, 1 ); callbacks.delete( item ); } } /** * Removes all queued items for which `filter` returns true. * @param {FilterCallback} filter - Called with each item; return true to remove */ removeByFilter( filter ) { const { items } = this; for ( let i = 0; i < items.length; i ++ ) { const item = items[ i ]; if ( filter( item ) ) { this.remove( item ); i --; } } } /** * Immediately attempts to dequeue and run pending jobs up to `maxJobs` concurrency. */ tryRunJobs() { this.sort(); const items = this.items; const callbacks = this.callbacks; const maxJobs = this.maxJobs; let iterated = 0; const completedCallback = () => { this.currJobs --; if ( this.autoUpdate ) { this.scheduleJobRun(); } }; while ( maxJobs > this.currJobs && items.length > 0 && iterated < maxJobs ) { this.currJobs ++; iterated ++; const item = items.pop(); const { callback, resolve, reject } = callbacks.get( item ); callbacks.delete( item ); let result; try { result = callback( item ); } catch ( err ) { reject( err ); completedCallback(); } if ( result instanceof Promise ) { result .then( resolve ) .catch( reject ) .finally( completedCallback ); } else { resolve( result ); completedCallback(); } } } /** * Immediately runs the callback for the given item, removing it from the queue. * Does nothing if the item is not queued. * @param {any} item * @returns {Promise<any>|any} */ flush( item ) { const { items, callbacks } = this; const index = items.indexOf( item ); if ( ! callbacks.has( item ) ) { return; } const { callback, resolve, reject } = callbacks.get( item ); callbacks.delete( item ); items.splice( index, 1 ); let result; try { result = callback( item ); } catch ( err ) { reject( err ); return; } if ( result instanceof Promise ) { result .then( resolve ) .catch( reject ); } else { resolve( result ); } return result; } /** * Schedules a deferred call to `tryRunJobs` via `schedulingCallback`. */ scheduleJobRun() { if ( ! this.scheduled ) { this._schedulingCallback( this._runjobs ); this.scheduled = true; } } }