var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { BehaviorSubject, map, timeout, throwError, of, catchError, EMPTY, mergeMap, debounceTime, merge, interval, filter, } from 'rxjs';
import * as R from 'ramda';
import { fetchIpfsContent } from '../ipfs/utils/utils-ipfs';
import { promiseToObservable } from '../utils/helpers';
import { serializeError } from 'serialize-error';
import { QueueStrategy } from './QueueStrategy';
import { QueueItemTimeoutError } from './QueueItemTimeoutError';
import BroadcastChannelSender from '../backend/channels/BroadcastChannelSender';
const QUEUE_DEBOUNCE_MS = 33;
const CONNECTION_KEEPER_RETRY_MS = 5000;
function getQueueItemTotalPriority(item) {
    return (item.priority || 0) + (item.viewPortPriority || 0);
}
const debugCid = (cid, prefix, ...args) => {
    console.log(`>>> ${prefix}: ${cid}`, ...args);
};
const strategies = {
    external: new QueueStrategy({
        db: { timeout: 5000, maxConcurrentExecutions: 999 },
        node: { timeout: 60 * 1000, maxConcurrentExecutions: 50 },
        gateway: { timeout: 21000, maxConcurrentExecutions: 11 },
    }, ['db', 'node', 'gateway']),
    embedded: new QueueStrategy({
        db: { timeout: 5000, maxConcurrentExecutions: 999 },
        node: { timeout: 60 * 1000, maxConcurrentExecutions: 50 },
        gateway: { timeout: 21000, maxConcurrentExecutions: 11 },
    }, ['db', 'gateway', 'node']),
    helia: new QueueStrategy({
        db: { timeout: 5000, maxConcurrentExecutions: 999 },
        node: { timeout: 60 * 1000, maxConcurrentExecutions: 50 },
        gateway: { timeout: 21000, maxConcurrentExecutions: 11 },
    }, ['db', 'node', 'gateway']),
};
class QueueManager {
    switchStrategy(strategy) {
        this.strategy = strategy;
    }
    setNode(node, customStrategy) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            console.log(`switch node from ${(_a = this.node) === null || _a === void 0 ? void 0 : _a.nodeType} to ${node.nodeType}`);
            this.node = node;
            this.switchStrategy(customStrategy || strategies[node.nodeType]);
        });
    }
    getItemBySourceAndPriority(queue) {
        const pendingItems = [...queue.values()].filter((i) => i.status === 'pending');
        const pendingBySource = R.groupBy((i) => i.source, pendingItems);
        const itemsToExecute = [];
        for (const [queueSource, items] of Object.entries(pendingBySource)) {
            const settings = this.strategy.settings[queueSource];
            const executeCount = settings.maxConcurrentExecutions -
                this.executing[queueSource].size;
            const itemsByPriority = items
                .sort((a, b) => getQueueItemTotalPriority(b) - getQueueItemTotalPriority(a))
                .slice(0, executeCount);
            itemsToExecute.push(...itemsByPriority);
        }
        return itemsToExecute;
    }
    postSummary() {
        const summary = `(total: ${this.queue$.value.size} |  db - ${this.executing.db.size} node - ${this.executing.node.size} gateway - ${this.executing.gateway.size})`;
        this.channel.postServiceStatus('ipfs', 'started', summary);
    }
    fetchData$(item) {
        const { cid, source, controller, callbacks } = item;
        const settings = this.strategy.settings[source];
        this.executing[source].add(cid);
        this.postSummary();
        const queueItem = this.queue$.value.get(cid);
        this.queue$.value.set(cid, Object.assign(Object.assign({}, queueItem), { status: 'executing', executionTime: Date.now(), controller: new AbortController() }));
        callbacks.map((callback) => callback(cid, 'executing', source));
        return promiseToObservable(() => __awaiter(this, void 0, void 0, function* () {
            return fetchIpfsContent(cid, source, {
                controller,
                node: this.node,
            }).then((content) => {
                var _a;
                (_a = this.defferedDbSaver) === null || _a === void 0 ? void 0 : _a.enqueueIpfsContent(content);
                return content;
            });
        })).pipe(timeout({
            each: settings.timeout,
            with: () => throwError(() => {
                controller === null || controller === void 0 ? void 0 : controller.abort('timeout');
                return new QueueItemTimeoutError(settings.timeout);
            }),
        }), map((result) => {
            return {
                item,
                status: result ? 'completed' : 'error',
                source,
                result,
            };
        }), catchError((error) => {
            const serializedError = serializeError(error);
            debugCid(cid, 'fetchData - fetchIpfsContent catchErr', JSON.stringify(serializedError, null, 2));
            if (error instanceof QueueItemTimeoutError) {
                return of({
                    item,
                    status: 'timeout',
                    source,
                });
            }
            if ((error === null || error === void 0 ? void 0 : error.name) === 'AbortError') {
                return of({ item, status: 'cancelled', source });
            }
            return of({ item, status: 'error', source });
        }));
    }
    mutateQueueItem(cid, changes) {
        const queue = this.queue$.value;
        const item = queue.get(cid);
        if (item) {
            queue.set(cid, Object.assign(Object.assign({}, item), changes));
        }
        return this.queue$.next(queue);
    }
    removeAndNext(cid) {
        const queue = this.queue$.value;
        queue.delete(cid);
        this.queue$.next(queue);
    }
    switchSourceAndNext(item, nextSource) {
        item.callbacks.map((callback) => callback(item.cid, 'pending', nextSource));
        this.mutateQueueItem(item.cid, { status: 'pending', source: nextSource });
    }
    cancelDeprioritizedItems(queue) {
        ['node', 'gateway'].forEach((source) => {
            Array.from(this.executing[source]).forEach((cid) => {
                const item = queue.get(cid);
                if (item && getQueueItemTotalPriority(item) < 0 && item.controller) {
                    item.controller.abort('cancelled');
                    item.callbacks.map((callback) => callback(item.cid, 'pending', item.source));
                    queue.set(cid, Object.assign(Object.assign({}, item), { status: 'pending' }));
                    this.executing[source].delete(cid);
                }
            });
        });
        return queue;
    }
    releaseExecution(cid) {
        Object.keys(this.executing).forEach((key) => this.executing[key].delete(cid));
    }
    constructor(ipfsInstance$, { strategy, queueDebounceMs, defferedDbSaver, }) {
        this.queue$ = new BehaviorSubject(new Map());
        this.node = undefined;
        this.lastNodeCallTime = Date.now();
        this.channel = new BroadcastChannelSender();
        this.executing = {
            db: new Set(),
            node: new Set(),
            gateway: new Set(),
        };
        ipfsInstance$.subscribe((node) => {
            if (node) {
                this.setNode(node);
            }
        });
        this.strategy = strategy || strategies.embedded;
        this.queueDebounceMs = queueDebounceMs || QUEUE_DEBOUNCE_MS;
        this.defferedDbSaver = defferedDbSaver;
        interval(CONNECTION_KEEPER_RETRY_MS)
            .pipe(filter(() => this.queue$.value.size > 0 && !!this.node))
            .subscribe(() => this.node.reconnectToSwarm(this.lastNodeCallTime));
        this.queue$
            .pipe(debounceTime(this.queueDebounceMs), map((queue) => this.cancelDeprioritizedItems(queue)), mergeMap((queue) => {
            var _a;
            const workItems = this.getItemBySourceAndPriority(queue);
            if (workItems.length > 0) {
                (_a = this.node) === null || _a === void 0 ? void 0 : _a.reconnectToSwarm(this.lastNodeCallTime);
                return merge(...workItems.map((item) => this.fetchData$(item)));
            }
            return EMPTY;
        }))
            .subscribe(({ item, status, source, result }) => {
            var _a;
            const { cid } = item;
            const callbacks = ((_a = this.queue$.value.get(cid)) === null || _a === void 0 ? void 0 : _a.callbacks) || [];
            callbacks.map((callback) => callback(cid, status, source, result));
            if (source === 'node') {
                this.lastNodeCallTime = Date.now();
            }
            this.executing[source].delete(cid);
            if (status === 'completed' || status === 'cancelled') {
                this.removeAndNext(cid);
            }
            else {
                const nextSource = this.strategy.getNextSource(source);
                if (nextSource) {
                    this.switchSourceAndNext(item, nextSource);
                }
                else {
                    this.removeAndNext(cid);
                    callbacks.map((callback) => callback(cid, 'not_found', source, result));
                }
            }
            this.postSummary();
        });
    }
    enqueue(cid, callback, options = {}) {
        const queue = this.queue$.value;
        const existingItem = queue.get(cid);
        if (existingItem) {
            this.mutateQueueItem(cid, {
                callbacks: [...existingItem.callbacks, callback],
            });
        }
        else {
            const source = options.initialSource || this.strategy.order[0];
            const item = Object.assign({ cid, callbacks: [callback], source, status: 'pending', postProcessing: true }, options);
            callback(cid, 'pending', source);
            queue.set(cid, item);
            this.queue$.next(queue);
        }
    }
    enqueueAndWait(cid, options = {}) {
        return new Promise((resolve) => {
            const callback = ((cid, status, source, result) => {
                if (status === 'completed' || status === 'not_found') {
                    resolve({ status, source, result });
                }
            });
            this.enqueue(cid, callback, options);
        });
    }
    updateViewPortPriority(cid, viewPortPriority) {
        this.mutateQueueItem(cid, { viewPortPriority });
    }
    cancel(cid) {
        const queue = this.queue$.value;
        const item = queue.get(cid);
        if (item) {
            if (!item.controller) {
                this.removeAndNext(cid);
            }
            else {
                item.controller.abort('cancelled');
            }
        }
    }
    cancelByParent(parent) {
        const queue = this.queue$.value;
        queue.forEach((item, cid) => {
            var _a;
            if (item.parent === parent) {
                this.releaseExecution(cid);
                (_a = item.controller) === null || _a === void 0 ? void 0 : _a.abort('cancelled');
                queue.delete(cid);
            }
        });
        this.queue$.next(queue);
    }
    clear() {
        const queue = this.queue$.value;
        queue.forEach((item, cid) => {
            var _a;
            this.releaseExecution(cid);
            (_a = item.controller) === null || _a === void 0 ? void 0 : _a.abort('cancelled');
            queue.delete(cid);
        });
        this.queue$.next(new Map());
    }
    getQueueMap() {
        return this.queue$.value;
    }
    getQueueList() {
        return Array.from(this.queue$.value.values());
    }
    getStats() {
        const fn = R.pipe(R.countBy(R.prop('status')), R.toPairs, R.map(R.zipObj(['status', 'count'])));
        return fn(this.getQueueList());
    }
}
export default QueueManager;
//# sourceMappingURL=QueueManager.js.map