@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
161 lines (157 loc) • 4.51 kB
JavaScript
/*
* 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 PromiseUtils from '../utils/PromiseUtils';
import OperationCounter from './OperationCounter';
function defaultShouldExecute() {
return true;
}
class Task {
constructor(id, priority, request, resolve, reject, shouldExecute, signal) {
this.id = id;
this._priority = priority;
this._signal = signal;
this._resolve = resolve;
this.reject = reject;
this._request = request;
this.shouldExecute = shouldExecute ?? defaultShouldExecute;
}
getKey() {
return this.id;
}
getPriority() {
if (this._signal?.aborted === true) {
// means "drop the request"
return Infinity;
}
return this._priority;
}
execute() {
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) {
return task.getPriority();
}
function keyFn(task) {
return task.getKey();
}
const MAX_CONCURRENT_REQUESTS = 10;
/**
* A generic priority queue that ensures that the same request cannot be added twice in the queue.
*/
export class RequestQueue extends EventDispatcher {
/**
* @param options - Options.
*/
constructor(options = {}) {
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;
}
get length() {
return this._queue.getCount();
}
get progress() {
return this._opCounter.progress;
}
get loading() {
return this._opCounter.loading;
}
get pendingRequests() {
return this._pendingIds.size;
}
get concurrentRequests() {
return this._concurrentRequests;
}
onQueueAvailable() {
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`.
*/
enqueue(options) {
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);
}
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;
}
}
/**
* A global singleton queue.
*/
export const DefaultQueue = new RequestQueue();
export default RequestQueue;
/**
* Defers the action by queueing it to the default queue.
*/
export function defer(action, signal) {
return DefaultQueue.enqueue({
id: MathUtils.generateUUID(),
request: () => Promise.resolve(action()),
shouldExecute: () => signal == null || !signal.aborted
});
}