UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

238 lines (200 loc) 7.05 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import PriorityQueue from 'ol/structs/PriorityQueue'; import { EventDispatcher, MathUtils } from 'three'; import type Progress from './Progress'; import PromiseUtils from '../utils/PromiseUtils'; import OperationCounter from './OperationCounter'; function defaultShouldExecute(): boolean { return true; } class Task { public readonly id: string; private readonly _priority: number; private readonly _signal?: AbortSignal; private readonly _resolve: (arg: unknown) => void; private readonly _request: () => Promise<unknown>; public readonly reject: (reason?: unknown) => void; public readonly shouldExecute: () => boolean; public constructor( id: string, priority: number, request: () => Promise<unknown>, resolve: (arg: unknown) => void, reject: (reason?: unknown) => void, shouldExecute: (() => boolean) | undefined, signal: AbortSignal | undefined, ) { this.id = id; this._priority = priority; this._signal = signal; this._resolve = resolve; this.reject = reject; this._request = request; this.shouldExecute = shouldExecute ?? defaultShouldExecute; } public getKey(): string { return this.id; } public getPriority(): number { if (this._signal?.aborted === true) { // means "drop the request" return Infinity; } return this._priority; } public execute(): Promise<unknown> { if (this._signal?.aborted === true) { this.reject(PromiseUtils.abortError()); return Promise.reject(); } return this._request() .then(x => this._resolve(x)) .catch(e => this.reject(e)); } } function priorityFn(task: Task): number { return task.getPriority(); } function keyFn(task: Task): string { return task.getKey(); } const MAX_CONCURRENT_REQUESTS = 10; export interface RequestQueueEvents { /** * Raised when a task has been executed. */ 'task-executed': unknown; /** * Raised when a task has been cancelled. */ 'task-cancelled': unknown; } /** * A generic priority queue that ensures that the same request cannot be added twice in the queue. */ export class RequestQueue extends EventDispatcher<RequestQueueEvents> implements Progress { private readonly _pendingIds: Map<string, Promise<unknown>>; private readonly _queue: PriorityQueue<Task>; private readonly _opCounter: OperationCounter; private readonly _maxConcurrentRequests: number; private _concurrentRequests: number; /** * @param options - Options. */ public constructor( options: { /** The maximum number of concurrent requests. */ maxConcurrentRequests?: number; } = {}, ) { super(); this._pendingIds = new Map(); this._queue = new PriorityQueue(priorityFn, keyFn); this._opCounter = new OperationCounter(); this._concurrentRequests = 0; this._maxConcurrentRequests = options.maxConcurrentRequests ?? MAX_CONCURRENT_REQUESTS; } public get length(): number { return this._queue.getCount(); } public get progress(): number { return this._opCounter.progress; } public get loading(): boolean { return this._opCounter.loading; } public get pendingRequests(): number { return this._pendingIds.size; } public get concurrentRequests(): number { return this._concurrentRequests; } public onQueueAvailable(): void { while (this._concurrentRequests < this._maxConcurrentRequests) { if (this._queue.isEmpty()) { break; } const task = this._queue.dequeue(); const key = task.getKey(); if (task.shouldExecute()) { this._concurrentRequests++; task.execute() .catch(e => task.reject(e)) .finally(() => { this._opCounter.decrement(); this._pendingIds.delete(key); this._concurrentRequests--; this.onQueueAvailable(); this.dispatchEvent({ type: 'task-executed' }); }); } else { this._opCounter.decrement(); this._pendingIds.delete(key); task.reject(PromiseUtils.abortError()); this.dispatchEvent({ type: 'task-cancelled' }); } } } /** * Enqueues a request. If a request with the same id is currently in the queue, then returns * the promise associated with the existing request. * * @param options - Options. * @returns A promise that resolves when the requested is completed. * @throws `AbortError` if the request is aborted before being started (either because * the `AbortSignal` became aborted, or if the `shouldExecute()` function returned `true`. */ public enqueue<T>(options: { /** The unique identifier of this request. */ id: string; /** The request. */ request: () => Promise<T>; /** The abort signal. */ signal?: AbortSignal; /** The priority of this request. */ priority?: number; /** The optional predicate used to discard a task: if the function returns `false`, * the task is not executed. */ shouldExecute?: () => boolean; }): Promise<T> { const { id, request, signal, shouldExecute } = options; const priority = options.priority ?? 0; if (signal?.aborted === true) { return Promise.reject(PromiseUtils.abortError()); } if (this._pendingIds.has(id)) { return this._pendingIds.get(id) as Promise<T>; } this._opCounter.increment(); const promise = new Promise((resolve, reject) => { const task = new Task(id, priority, request, resolve, reject, shouldExecute, signal); if (this._queue.isEmpty()) { this._queue.enqueue(task); this.onQueueAvailable(); } else { this._queue.enqueue(task); } }); this._pendingIds.set(id, promise); return promise as Promise<T>; } } /** * A global singleton queue. */ export const DefaultQueue: RequestQueue = new RequestQueue(); export default RequestQueue; /** * Defers the action by queueing it to the default queue. */ export function defer<T>(action: () => T, signal?: AbortSignal): Promise<T> { return DefaultQueue.enqueue({ id: MathUtils.generateUUID(), request: () => Promise.resolve(action()), shouldExecute: () => signal == null || !signal.aborted, }); }