"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GapFiller = void 0;
const utils_1 = require("@streamr/utils");
const sample_1 = __importDefault(require("lodash/sample"));
const logger = new utils_1.Logger(module);
const runAbortableTask = async (run) => {
    try {
        await run();
    }
    catch (e) {
        if (e.code !== 'AbortError') {
            throw e;
        }
    }
};
class GapFiller {
    currentTask = undefined;
    chain;
    resend;
    getStorageNodeAddresses;
    strategy;
    initialWaitTime;
    retryWaitTime;
    maxRequestsPerGap;
    constructor(opts) {
        this.chain = opts.chain;
        this.resend = opts.resend;
        this.getStorageNodeAddresses = opts.getStorageNodeAddresses;
        this.strategy = opts.strategy;
        this.initialWaitTime = opts.initialWaitTime;
        this.retryWaitTime = opts.retryWaitTime;
        this.maxRequestsPerGap = opts.maxRequestsPerGap;
        opts.abortSignal.addEventListener('abort', () => {
            this.currentTask?.abortController.abort();
        });
    }
    start() {
        this.chain.on('gapFound', (gap) => this.onGapFound(gap));
        this.chain.on('gapResolved', () => this.onGapResolved());
    }
    async onGapFound(gap) {
        const abortController = new AbortController();
        this.currentTask = {
            gap,
            abortController
        };
        try {
            await runAbortableTask(async () => {
                await (0, utils_1.wait)(this.initialWaitTime, abortController.signal);
                if (this.maxRequestsPerGap > 0) {
                    await this.fetchFromStorageNode(gap, abortController.signal);
                }
                /*
                 * The "fetchFromStorageNode" typically provides all the missing messages and the chain emits the
                 * "gapResolved" event. In that case "chain.on('gapResolved')" callback aborts this task
                 * before we reach this line. Alternatively the callback may have been called by the chain because
                 * it received the missing messages from another source (i.e. the real-time pipeline). But if
                 * this task has not been aborted by either of these reasons, we resolve the gap manually as we
                 * don't try to fill it anymore.
                 */
                if (this.strategy === 'full') {
                    this.chain.resolveMessages(gap.to.getMessageRef(), true);
                }
                else if (this.strategy === 'light') {
                    this.chain.resolveMessages(undefined, false);
                }
                else {
                    throw new Error(`Assertion failed: unexpected gap fill strategy "${this.strategy}"`);
                }
            });
        }
        catch (err) {
            this.onError(err, gap);
        }
    }
    onGapResolved() {
        if (this.currentTask !== undefined) {
            this.currentTask.abortController.abort();
            this.currentTask = undefined;
        }
    }
    async fetchFromStorageNode(gap, abortSignal) {
        const addresses = await this.getStorageNodeAddresses();
        if (addresses.length > 0) {
            for (let i = 0; i < this.maxRequestsPerGap; i++) {
                try {
                    await runAbortableTask(async () => {
                        const msgs = this.resend(gap, (0, sample_1.default)(addresses), abortSignal);
                        for await (const msg of msgs) {
                            this.chain.addMessage(msg);
                        }
                    });
                }
                catch (err) {
                    this.onError(err, gap);
                }
                if (i !== this.maxRequestsPerGap - 1) {
                    await (0, utils_1.wait)(this.retryWaitTime, abortSignal);
                }
            }
        }
    }
    onError(error, gap) {
        logger.debug('Unable to fill gap', {
            error: {
                message: error?.message,
                code: error?.code,
            },
            context: this.chain.getContext(),
            from: gap.from.getMessageRef(),
            to: gap.to.getMessageRef()
        });
    }
}
exports.GapFiller = GapFiller;
//# sourceMappingURL=GapFiller.js.map