utils/Loader.tstypescript
import { createEvents } from "../Events";
import type { EventsHandler } from "../Events";
import { Planet } from "../scene/Planet";
import { Segment } from "../segment/Segment";
import { QuadTreeStrategy } from "../quadTree";

type LoaderEventsList = ["loadend", "layerloadend"];

const LOADER_EVENTS: LoaderEventsList = ["loadend", "layerloadend"];

export interface IResponse {
    status: string;
    data?: any;
    msg?: string;
}

type IResponseCallback = (response: IResponse) => void;

export type FetchCache = "default" | "no-store" | "no-cache" | "reload" | "force-cache" | "only-if-cached";

// interface IResponseHandler {
//     json: (r: Response) => Promise<any>;
//     blob: (r: Response) => Promise<Blob>;
//     arrayBuffer: (r: Response) => Promise<ArrayBuffer>;
//     imageBitmap: (r: Response) => Promise<ImageBitmap>;
//     text: (r: Response) => Promise<string>;
// }

interface Obj<T> {
    __id: number;
    isIdle: boolean;
    isEqual: (obj: T) => boolean;
    events: EventsHandler<any>;
    _planet?: Planet | null;
    quadTreeStrategy?: QuadTreeStrategy | null;
}

type QueryParams<T> = {
    sender?: T;
    src: string;
    type: string;
    filter?: (val: QueryParams<T>) => boolean;
    options?: any;
    segment?: Segment;
};

type QueueData<T> = {
    params: QueryParams<T>;
    callback: IResponseCallback;
};

type RequestCounter<T> = {
    sender: T;
    counter: number;
    __requestCounterFrame__: number;
};

export class Loader<T extends Obj<T>> {
    public MAX_REQUESTS: number;

    public events: EventsHandler<LoaderEventsList>;

    protected _loading: number;

    protected _queue: QueueData<T>[]; //new QueueArray();

    protected _senderRequestCounter: RequestCounter<T>[];

    protected _promises: { [key: string]: (r: Response) => Promise<any> }; //IResponseHandler;

    constructor(maxRequests: number = 24) {
        this.MAX_REQUESTS = maxRequests;

        this.events = createEvents(LOADER_EVENTS);

        this._loading = 0;

        this._queue = []; //new QueueArray();

        this._senderRequestCounter = [];

        this._promises = {
            json: (r) => r.json(),
            blob: (r) => r.blob(),
            arrayBuffer: (r) => r.arrayBuffer(),
            imageBitmap: (r) =>
                r.blob().then((r: Blob) =>
                    createImageBitmap(r, {
                        premultiplyAlpha: "premultiply"
                    })
                ),
            text: (r) => r.text()
        };
    }

    public load(params: QueryParams<T>, callback: IResponseCallback) {
        if (params.sender) {
            if (!this._senderRequestCounter[params.sender.__id]) {
                this._senderRequestCounter[params.sender.__id] = {
                    sender: params.sender,
                    counter: 0,
                    __requestCounterFrame__: 0
                };
            }
            this._senderRequestCounter[params.sender.__id].counter++;
        }
        this._queue.push({ params, callback });
        this._exec();
    }

    public fetch(params: QueryParams<T>) {
        return fetch(params.src, params.options || {})
            .then((response: Response) => {
                if (!response.ok) {
                    throw Error(`Unable to load '${params.src}'`);
                }
                return this._promises[params.type || "blob"](response);
            })

            .then((data: any) => {
                return { status: "ready", data: data };
            })

            .catch((err: Error) => {
                return { status: "error", msg: err.toString() };
            });
    }

    public getRequestCounter(sender: T): number {
        if (sender) {
            let r = this._senderRequestCounter[sender.__id];
            if (r) {
                return r.counter;
            }
        }
        return 0;
    }

    public isIdle(sender: T): boolean {
        return sender.isIdle;
    }

    protected _checkLoadend(request: RequestCounter<T>, sender: T) {
        if (
            request.counter === 0 &&
            (!sender._planet || (sender.quadTreeStrategy && sender.quadTreeStrategy._terrainCompletedActivated))
        ) {
            sender.events.dispatch(sender.events.loadend, sender);
            this.events.dispatch(this.events.layerloadend, sender);
            request.__requestCounterFrame__ = 0;
        } else {
            request.__requestCounterFrame__ = requestAnimationFrame(() => {
                this._checkLoadend(request, sender);
            });
        }
    }

    protected _handleResponse(q: QueueData<T>, response: IResponse) {
        q.callback(response);
        let sender = q.params.sender;
        if (sender && (sender.events.loadend!.handlers.length || this.events.layerloadend!.handlers.length)) {
            let request = this._senderRequestCounter[sender.__id];
            if (request && request.counter > 0) {
                request.counter--;
                cancelAnimationFrame(request.__requestCounterFrame__!);
                request.__requestCounterFrame__ = requestAnimationFrame(() => {
                    this._checkLoadend(request, sender!);
                });
            }
        }
        this._exec();
    }

    protected _exec() {
        if (this._queue.length > 0 && this._loading < this.MAX_REQUESTS) {
            let q = this._queue.pop()!;

            if (!q) return;

            let p = q.params;

            if (!p.filter || p.filter(p)) {
                this._loading++;

                return fetch(p.src!, p.options || {})
                    .then((response: Response) => {
                        if (!response.ok) {
                            throw Error(`Unable to load '${p.src}'`);
                        }
                        return this._promises[p.type || "blob"](response);
                    })
                    .then((data: any) => {
                        this._loading--;
                        this._handleResponse(q, { status: "ready", data: data });
                    })
                    .catch((err: Error) => {
                        this._loading--;
                        this._handleResponse(q, { status: "error", msg: err.toString() });
                    });
            } else {
                this._handleResponse(q, { status: "abort" });
            }
        } else if (this._loading === 0) {
            this.events.dispatch(this.events.loadend);
        }
    }

    public abort(sender: T) {
        if (this._senderRequestCounter[sender.__id]) {
            this._senderRequestCounter[sender.__id].counter = 0;
            cancelAnimationFrame(this._senderRequestCounter[sender.__id].__requestCounterFrame__);
            this._senderRequestCounter[sender.__id].__requestCounterFrame__ = 0;
        }

        for (let i = 0, len = this._queue.length; i < len; i++) {
            let qi = this._queue[i];
            if (qi && qi.params.sender && sender.isEqual(qi.params.sender)) {
                qi.callback({ status: "abort" });
                //@ts-ignore
                this._queue[i] = null;
            }
        }
    }

    public abortAll() {
        for (let i = 0, len = this._queue.length; i < len; i++) {
            let qi = this._queue[i];
            if (qi) {
                let sender = qi.params.sender;
                if (sender && this._senderRequestCounter[sender.__id]) {
                    this._senderRequestCounter[sender.__id].counter = 0;
                    cancelAnimationFrame(this._senderRequestCounter[sender.__id].__requestCounterFrame__);
                    this._senderRequestCounter[sender.__id].__requestCounterFrame__ = 0;
                }
                qi.callback({ status: "abort" });
                //@ts-ignore
                this._queue[i] = null;
            }
        }
        this._queue = [];
    }

    public get loading(): number {
        return this._loading;
    }

    /**
     * Returns true when there is nothing being loaded and nothing left in the queue.
     * @public
     * @returns {boolean}
     */
    public get isFree(): boolean {
        return this._loading === 0 && this._queue.length === 0;
    }

    public get queue(): QueueData<T>[] {
        return this._queue;
    }
}